mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-18 18:12:37 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
23
Dockerfile
23
Dockerfile
@@ -1,19 +1,28 @@
|
|||||||
FROM registry.fit2cloud.com/public/python:v3
|
FROM registry.fit2cloud.com/public/python:v3 as stage-build
|
||||||
MAINTAINER Jumpserver Team <ibuler@qq.com>
|
MAINTAINER Jumpserver Team <ibuler@qq.com>
|
||||||
|
ARG VERSION
|
||||||
|
ENV VERSION=$VERSION
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
WORKDIR /opt/jumpserver
|
||||||
RUN useradd jumpserver
|
ADD . .
|
||||||
|
RUN cd utils && bash -ixeu build.sh
|
||||||
|
|
||||||
COPY ./requirements /tmp/requirements
|
|
||||||
|
FROM registry.fit2cloud.com/public/python:v3
|
||||||
|
WORKDIR /opt/jumpserver
|
||||||
|
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||||
|
|
||||||
|
RUN useradd jumpserver
|
||||||
|
|
||||||
RUN yum -y install epel-release && \
|
RUN yum -y install epel-release && \
|
||||||
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||||
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
|
|
||||||
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
|
COPY . .
|
||||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt
|
RUN yum -y install $(cat requirements/rpm_requirements.txt)
|
||||||
|
RUN pip install --upgrade pip setuptools && pip install wheel && \
|
||||||
|
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements/requirements.txt || pip install -r requirements/requirements.txt
|
||||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||||
|
|
||||||
COPY . /opt/jumpserver
|
|
||||||
RUN echo > config.yml
|
RUN echo > config.yml
|
||||||
VOLUME /opt/jumpserver/data
|
VOLUME /opt/jumpserver/data
|
||||||
VOLUME /opt/jumpserver/logs
|
VOLUME /opt/jumpserver/logs
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from .. import serializers
|
|||||||
from ..tasks import (
|
from ..tasks import (
|
||||||
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
update_asset_hardware_info_manual, test_asset_connectivity_manual
|
||||||
)
|
)
|
||||||
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
|
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
@@ -32,7 +32,7 @@ class AssetViewSet(OrgBulkModelViewSet):
|
|||||||
model = Asset
|
model = Asset
|
||||||
filter_fields = (
|
filter_fields = (
|
||||||
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
|
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
|
||||||
"is_active"
|
"is_active", 'ip'
|
||||||
)
|
)
|
||||||
search_fields = ("hostname", "ip")
|
search_fields = ("hostname", "ip")
|
||||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||||
@@ -41,7 +41,7 @@ class AssetViewSet(OrgBulkModelViewSet):
|
|||||||
'display': serializers.AssetDisplaySerializer,
|
'display': serializers.AssetDisplaySerializer,
|
||||||
}
|
}
|
||||||
permission_classes = (IsOrgAdminOrAppUser,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
|
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||||
|
|
||||||
def set_assets_node(self, assets):
|
def set_assets_node(self, assets):
|
||||||
if not isinstance(assets, list):
|
if not isinstance(assets, list):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
import coreapi
|
from rest_framework.compat import coreapi, coreschema
|
||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
@@ -117,3 +117,23 @@ class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
|
|||||||
def perform_query(pattern, queryset):
|
def perform_query(pattern, queryset):
|
||||||
return queryset.filter(asset__nodes__key__regex=pattern).distinct()
|
return queryset.filter(asset__nodes__key__regex=pattern).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
class IpInFilterBackend(filters.BaseFilterBackend):
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
ips = request.query_params.get('ips')
|
||||||
|
if not ips:
|
||||||
|
return queryset
|
||||||
|
ip_list = [i.strip() for i in ips.split(',')]
|
||||||
|
queryset = queryset.filter(ip__in=ip_list)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_schema_fields(self, view):
|
||||||
|
return [
|
||||||
|
coreapi.Field(
|
||||||
|
name='ips', location='query', required=False, type='string',
|
||||||
|
schema=coreschema.String(
|
||||||
|
title='ips',
|
||||||
|
description='ip in filter'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|||||||
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'))
|
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
|
||||||
|
|
||||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
|
||||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||||
|
|
||||||
@@ -244,10 +244,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
|||||||
def platform_base(self):
|
def platform_base(self):
|
||||||
return self.platform.base
|
return self.platform.base
|
||||||
|
|
||||||
@lazyproperty
|
|
||||||
def admin_user_display(self):
|
|
||||||
return self.admin_user.name
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def admin_user_username(self):
|
def admin_user_username(self):
|
||||||
"""求可连接性时,直接用用户名去取,避免再查一次admin user
|
"""求可连接性时,直接用用户名去取,避免再查一次admin user
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.core.cache import cache
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orgs.mixins.models import OrgManager
|
from orgs.mixins.models import OrgManager
|
||||||
@@ -59,19 +58,17 @@ class AuthBook(BaseUser):
|
|||||||
"""
|
"""
|
||||||
username = kwargs['username']
|
username = kwargs['username']
|
||||||
asset = kwargs['asset']
|
asset = kwargs['asset']
|
||||||
key_lock = 'KEY_LOCK_CREATE_AUTH_BOOK_{}_{}'.format(username, asset.id)
|
with transaction.atomic():
|
||||||
with cache.lock(key_lock):
|
# 使用select_for_update限制并发创建相同的username、asset条目
|
||||||
with transaction.atomic():
|
instances = cls.objects.select_for_update().filter(username=username, asset=asset)
|
||||||
cls.objects.filter(
|
instances.filter(is_latest=True).update(is_latest=False)
|
||||||
username=username, asset=asset, is_latest=True
|
max_version = cls.get_max_version(username, asset)
|
||||||
).update(is_latest=False)
|
kwargs.update({
|
||||||
max_version = cls.get_max_version(username, asset)
|
'version': max_version + 1,
|
||||||
kwargs.update({
|
'is_latest': True
|
||||||
'version': max_version + 1,
|
})
|
||||||
'is_latest': True
|
obj = cls.objects.create(**kwargs)
|
||||||
})
|
return obj
|
||||||
obj = cls.objects.create(**kwargs)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connectivity(self):
|
def connectivity(self):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ __all__ = [
|
|||||||
|
|
||||||
class CommandFilter(OrgModelMixin):
|
class CommandFilter(OrgModelMixin):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
name = models.CharField(max_length=64, unique=True, verbose_name=_("Name"))
|
name = models.CharField(max_length=64, verbose_name=_("Name"))
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||||
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
|
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
|
||||||
date_created = models.DateTimeField(auto_now_add=True)
|
date_created = models.DateTimeField(auto_now_add=True)
|
||||||
@@ -29,6 +29,7 @@ class CommandFilter(OrgModelMixin):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
unique_together = [('org_id', 'name')]
|
||||||
verbose_name = _("Command filter")
|
verbose_name = _("Command filter")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ __all__ = ['Domain', 'Gateway']
|
|||||||
|
|
||||||
class Domain(OrgModelMixin):
|
class Domain(OrgModelMixin):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||||
date_created = models.DateTimeField(auto_now_add=True, null=True,
|
date_created = models.DateTimeField(auto_now_add=True, null=True,
|
||||||
verbose_name=_('Date created'))
|
verbose_name=_('Date created'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Domain")
|
verbose_name = _("Domain")
|
||||||
|
unique_together = [('org_id', 'name')]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -199,6 +199,20 @@ class FamilyMixin:
|
|||||||
)
|
)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
|
def get_or_create_child(self, value, _id=None):
|
||||||
|
"""
|
||||||
|
:return: Node, bool (created)
|
||||||
|
"""
|
||||||
|
children = self.get_children()
|
||||||
|
exist = children.filter(value=value).exists()
|
||||||
|
if exist:
|
||||||
|
child = children.filter(value=value).first()
|
||||||
|
created = False
|
||||||
|
else:
|
||||||
|
child = self.create_child(value, _id)
|
||||||
|
created = True
|
||||||
|
return child, created
|
||||||
|
|
||||||
def get_next_child_key(self):
|
def get_next_child_key(self):
|
||||||
mark = self.child_mark
|
mark = self.child_mark
|
||||||
self.child_mark += 1
|
self.child_mark += 1
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
|||||||
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
|
||||||
)
|
)
|
||||||
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
protocols = ProtocolsField(label=_('Protocols'), required=False)
|
||||||
|
domain_display = serializers.ReadOnlyField(source='domain.name')
|
||||||
|
admin_user_display = serializers.ReadOnlyField(source='admin_user.name')
|
||||||
|
|
||||||
"""
|
"""
|
||||||
资产的数据结构
|
资产的数据结构
|
||||||
"""
|
"""
|
||||||
@@ -82,7 +85,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
|||||||
'created_by', 'date_created', 'hardware_info',
|
'created_by', 'date_created', 'hardware_info',
|
||||||
]
|
]
|
||||||
fields_fk = [
|
fields_fk = [
|
||||||
'admin_user', 'admin_user_display', 'domain', 'platform'
|
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
|
||||||
]
|
]
|
||||||
fk_only_fields = {
|
fk_only_fields = {
|
||||||
'platform': ['name']
|
'platform': ['name']
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None
|
|||||||
|
|
||||||
system_users_assets = defaultdict(set)
|
system_users_assets = defaultdict(set)
|
||||||
for system_user in system_users:
|
for system_user in system_users:
|
||||||
system_users_assets[system_user].update(set(assets))
|
assets_has_set = system_user.assets.all().filter(id__in=assets).values_list('id', flat=True)
|
||||||
|
assets_remain = set(assets) - set(assets_has_set)
|
||||||
|
system_users_assets[system_user].update(assets_remain)
|
||||||
for system_user, _assets in system_users_assets.items():
|
for system_user, _assets in system_users_assets.items():
|
||||||
system_user.assets.add(*tuple(_assets))
|
system_user.assets.add(*tuple(_assets))
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def test_asset_user_connectivity_util(asset_user, task_name):
|
|||||||
raw, summary = test_user_connectivity(
|
raw, summary = test_user_connectivity(
|
||||||
task_name=task_name, asset=asset_user.asset,
|
task_name=task_name, asset=asset_user.asset,
|
||||||
username=asset_user.username, password=asset_user.password,
|
username=asset_user.username, password=asset_user.password,
|
||||||
private_key=asset_user.private_key
|
private_key=asset_user.private_key_file
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
|
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ GATHER_ASSET_USERS_TASKS = [
|
|||||||
"action": {
|
"action": {
|
||||||
"module": "shell",
|
"module": "shell",
|
||||||
"args": "users=$(getent passwd | grep -v 'nologin' | "
|
"args": "users=$(getent passwd | grep -v 'nologin' | "
|
||||||
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | "
|
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -w -F $i -1 | "
|
||||||
"head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
|
"head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
|||||||
"""
|
"""
|
||||||
from ops.utils import update_or_create_ansible_task
|
from ops.utils import update_or_create_ansible_task
|
||||||
|
|
||||||
hosts = clean_ansible_task_hosts(assets, system_user=system_user)
|
# hosts = clean_ansible_task_hosts(assets, system_user=system_user)
|
||||||
|
# TODO: 这里不传递系统用户,因为clean_ansible_task_hosts会通过system_user来判断是否可以推送,
|
||||||
|
# 不符合测试可连接性逻辑, 后面需要优化此逻辑
|
||||||
|
hosts = clean_ansible_task_hosts(assets)
|
||||||
if not hosts:
|
if not hosts:
|
||||||
return {}
|
return {}
|
||||||
platform_hosts_map = {}
|
platform_hosts_map = {}
|
||||||
|
|||||||
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):
|
class FTPLog(OrgModelMixin):
|
||||||
|
OPERATE_DELETE = 'Delete'
|
||||||
|
OPERATE_UPLOAD = 'Upload'
|
||||||
|
OPERATE_DOWNLOAD = 'Download'
|
||||||
|
OPERATE_RMDIR = 'Rmdir'
|
||||||
|
OPERATE_RENAME = 'Rename'
|
||||||
|
OPERATE_MKDIR = 'Mkdir'
|
||||||
|
OPERATE_SYMLINK = 'Symlink'
|
||||||
|
|
||||||
|
OPERATE_CHOICES = (
|
||||||
|
(OPERATE_DELETE, _('Delete')),
|
||||||
|
(OPERATE_UPLOAD, _('Upload')),
|
||||||
|
(OPERATE_DOWNLOAD, _('Download')),
|
||||||
|
(OPERATE_RMDIR, _('Rmdir')),
|
||||||
|
(OPERATE_RENAME, _('Rename')),
|
||||||
|
(OPERATE_MKDIR, _('Mkdir')),
|
||||||
|
(OPERATE_SYMLINK, _('Symlink'))
|
||||||
|
)
|
||||||
|
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
user = models.CharField(max_length=128, verbose_name=_('User'))
|
user = models.CharField(max_length=128, verbose_name=_('User'))
|
||||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||||
asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
|
asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
|
||||||
system_user = models.CharField(max_length=128, verbose_name=_("System user"))
|
system_user = models.CharField(max_length=128, verbose_name=_("System user"))
|
||||||
operate = models.CharField(max_length=16, verbose_name=_("Operate"))
|
operate = models.CharField(max_length=16, verbose_name=_("Operate"), choices=OPERATE_CHOICES)
|
||||||
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
|
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
|
||||||
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
||||||
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))
|
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ from . import models
|
|||||||
|
|
||||||
|
|
||||||
class FTPLogSerializer(serializers.ModelSerializer):
|
class FTPLogSerializer(serializers.ModelSerializer):
|
||||||
|
operate_display = serializers.ReadOnlyField(source='get_operate_display')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FTPLog
|
model = models.FTPLog
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'user', 'remote_addr', 'asset', 'system_user',
|
'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id',
|
||||||
'operate', 'filename', 'is_success', 'date_start'
|
'operate', 'filename', 'is_success', 'date_start', 'operate_display'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ class OperateLogSerializer(serializers.ModelSerializer):
|
|||||||
model = models.OperateLog
|
model = models.OperateLog
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'user', 'action', 'resource_type', 'resource',
|
'id', 'user', 'action', 'resource_type', 'resource',
|
||||||
'remote_addr', 'datetime'
|
'remote_addr', 'datetime', 'org_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
|||||||
fields_mini = ['id']
|
fields_mini = ['id']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'run_as', 'command', 'user', 'is_finished',
|
'run_as', 'command', 'user', 'is_finished',
|
||||||
'date_start', 'result', 'is_success'
|
'date_start', 'result', 'is_success', 'org_id'
|
||||||
]
|
]
|
||||||
fields = fields_small + ['hosts', 'run_as_display', 'user_display']
|
fields = fields_small + ['hosts', 'run_as_display', 'user_display']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ User = get_user_model()
|
|||||||
|
|
||||||
|
|
||||||
class CreateUserMixin:
|
class CreateUserMixin:
|
||||||
def get_django_user(self, username, password=None):
|
def get_django_user(self, username, password=None, *args, **kwargs):
|
||||||
if isinstance(username, bytes):
|
if isinstance(username, bytes):
|
||||||
username = username.decode()
|
username = username.decode()
|
||||||
try:
|
try:
|
||||||
@@ -27,6 +27,12 @@ class CreateUserMixin:
|
|||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
def authenticate(self, *args, **kwargs):
|
||||||
|
# 校验用户时,会传入public_key参数,父类authentication中不接受public_key参数,所以要pop掉
|
||||||
|
# TODO:需要优化各backend的authenticate方法,django进行调用前会检测各authenticate的参数
|
||||||
|
kwargs.pop('public_key', None)
|
||||||
|
return super().authenticate(*args, *kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RadiusBackend(CreateUserMixin, RADIUSBackend):
|
class RadiusBackend(CreateUserMixin, RADIUSBackend):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from users.utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
reason_password_failed = 'password_failed'
|
reason_password_failed = 'password_failed'
|
||||||
|
reason_password_decrypt_failed = 'password_decrypt_failed'
|
||||||
reason_mfa_failed = 'mfa_failed'
|
reason_mfa_failed = 'mfa_failed'
|
||||||
reason_mfa_unset = 'mfa_unset'
|
reason_mfa_unset = 'mfa_unset'
|
||||||
reason_user_not_exist = 'user_not_exist'
|
reason_user_not_exist = 'user_not_exist'
|
||||||
@@ -19,6 +20,7 @@ reason_user_inactive = 'user_inactive'
|
|||||||
|
|
||||||
reason_choices = {
|
reason_choices = {
|
||||||
reason_password_failed: _('Username/password check failed'),
|
reason_password_failed: _('Username/password check failed'),
|
||||||
|
reason_password_decrypt_failed: _('Password decrypt failed'),
|
||||||
reason_mfa_failed: _('MFA failed'),
|
reason_mfa_failed: _('MFA failed'),
|
||||||
reason_mfa_unset: _('MFA unset'),
|
reason_mfa_unset: _('MFA unset'),
|
||||||
reason_user_not_exist: _("Username does not exist"),
|
reason_user_not_exist: _("Username does not exist"),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class UserLoginForm(forms.Form):
|
|||||||
username = forms.CharField(label=_('Username'), max_length=100)
|
username = forms.CharField(label=_('Username'), max_length=100)
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
label=_('Password'), widget=forms.PasswordInput,
|
label=_('Password'), widget=forms.PasswordInput,
|
||||||
max_length=128, strip=False
|
max_length=1024, strip=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def confirm_login_allowed(self, user):
|
def confirm_login_allowed(self, user):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form class="m-t" role="form" method="post" action="">
|
<form id="form" class="m-t" role="form" method="post" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div style="line-height: 17px;">
|
<div style="line-height: 17px;">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
<input type="password" class="form-control" id="password" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
||||||
{% if form.errors.password %}
|
{% if form.errors.password %}
|
||||||
<div class="help-block field-error">
|
<div class="help-block field-error">
|
||||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{{ form.captcha }}
|
{{ form.captcha }}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Login' %}</button>
|
<button type="submit" class="btn btn-primary block full-width m-b" onclick="doLogin();return false;">{% trans 'Login' %}</button>
|
||||||
|
|
||||||
{% if demo_mode %}
|
{% if demo_mode %}
|
||||||
<p class="text-muted font-bold" style="color: red">
|
<p class="text-muted font-bold" style="color: red">
|
||||||
@@ -64,4 +64,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function encryptLoginPassword(password, rsaPublicKey){
|
||||||
|
var jsencrypt = new JSEncrypt(); //加密对象
|
||||||
|
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
|
||||||
|
return jsencrypt.encrypt(password); //加密
|
||||||
|
}
|
||||||
|
function doLogin() {
|
||||||
|
//公钥加密
|
||||||
|
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||||
|
var password =$('#password').val(); //明文密码
|
||||||
|
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||||
|
$('#password').val(passwordEncrypted); //返回给密码输入input
|
||||||
|
$('#form').submit();//post提交
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
<input type="password" class="form-control" id="password" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
|
||||||
{% if form.errors.password %}
|
{% if form.errors.password %}
|
||||||
<div class="help-block field-error">
|
<div class="help-block field-error">
|
||||||
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
{{ form.captcha }}
|
{{ form.captcha }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-top: 10px">
|
<div class="form-group" style="margin-top: 10px">
|
||||||
<button type="submit" class="btn btn-transparent">{% trans 'Login' %}</button>
|
<button type="submit" class="btn btn-transparent" onclick="doLogin();return false;">{% trans 'Login' %}</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
<a href="{% url 'authentication:forgot-password' %}">
|
<a href="{% url 'authentication:forgot-password' %}">
|
||||||
@@ -127,4 +127,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function encryptLoginPassword(password, rsaPublicKey){
|
||||||
|
var jsencrypt = new JSEncrypt(); //加密对象
|
||||||
|
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
|
||||||
|
return jsencrypt.encrypt(password); //加密
|
||||||
|
}
|
||||||
|
function doLogin() {
|
||||||
|
//公钥加密
|
||||||
|
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||||
|
var password =$('#password').val(); //明文密码
|
||||||
|
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||||
|
$('#password').val(passwordEncrypted); //返回给密码输入input
|
||||||
|
$('#contact-form').submit();//post提交
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
import base64
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Cipher import PKCS1_v1_5
|
||||||
|
from Crypto import Random
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_key_pair():
|
||||||
|
""" 生成加密key
|
||||||
|
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
|
||||||
|
"""
|
||||||
|
random_generator = Random.new().read
|
||||||
|
rsa = RSA.generate(1024, random_generator)
|
||||||
|
rsa_private_key = rsa.exportKey().decode()
|
||||||
|
rsa_public_key = rsa.publickey().exportKey().decode()
|
||||||
|
return rsa_private_key, rsa_public_key
|
||||||
|
|
||||||
|
|
||||||
|
def rsa_encrypt(message, rsa_public_key):
|
||||||
|
""" 加密登录密码 """
|
||||||
|
key = RSA.importKey(rsa_public_key)
|
||||||
|
cipher = PKCS1_v1_5.new(key)
|
||||||
|
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
|
||||||
|
return cipher_text
|
||||||
|
|
||||||
|
|
||||||
|
def rsa_decrypt(cipher_text, rsa_private_key=None):
|
||||||
|
""" 解密登录密码 """
|
||||||
|
if rsa_private_key is None:
|
||||||
|
# rsa_private_key 为 None,可以能是API请求认证,不需要解密
|
||||||
|
return cipher_text
|
||||||
|
key = RSA.importKey(rsa_private_key)
|
||||||
|
cipher = PKCS1_v1_5.new(key)
|
||||||
|
message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode()
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
def check_user_valid(**kwargs):
|
def check_user_valid(**kwargs):
|
||||||
password = kwargs.pop('password', None)
|
password = kwargs.pop('password', None)
|
||||||
@@ -11,6 +49,16 @@ def check_user_valid(**kwargs):
|
|||||||
username = kwargs.pop('username', None)
|
username = kwargs.pop('username', None)
|
||||||
request = kwargs.get('request')
|
request = kwargs.get('request')
|
||||||
|
|
||||||
|
# 获取解密密钥,对密码进行解密
|
||||||
|
rsa_private_key = request.session.get('rsa_private_key')
|
||||||
|
if rsa_private_key is not None:
|
||||||
|
try:
|
||||||
|
password = rsa_decrypt(password, rsa_private_key)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e, exc_info=True)
|
||||||
|
logger.error('Need decrypt password => {}'.format(password))
|
||||||
|
return None, errors.reason_password_decrypt_failed
|
||||||
|
|
||||||
user = authenticate(request, username=username,
|
user = authenticate(request, username=username,
|
||||||
password=password, public_key=public_key)
|
password=password, public_key=public_key)
|
||||||
if not user:
|
if not user:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from common.utils import get_request_ip, get_object_or_none
|
|||||||
from users.utils import (
|
from users.utils import (
|
||||||
redirect_user_first_login_or_index
|
redirect_user_first_login_or_index
|
||||||
)
|
)
|
||||||
from .. import forms, mixins, errors
|
from .. import forms, mixins, errors, utils
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -108,9 +108,13 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||||||
return self.form_class
|
return self.form_class
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||||
|
rsa_private_key, rsa_public_key = utils.gen_key_pair()
|
||||||
|
self.request.session['rsa_private_key'] = rsa_private_key
|
||||||
context = {
|
context = {
|
||||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||||
|
'rsa_public_key': rsa_public_key.replace('\n', '\\n')
|
||||||
}
|
}
|
||||||
kwargs.update(context)
|
kwargs.update(context)
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|||||||
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 codecs
|
||||||
import unicodecsv
|
import unicodecsv
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework.parsers import BaseParser
|
from rest_framework.parsers import BaseParser
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError, APIException
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class CsvDataTooBig(APIException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_code = 'csv_data_too_big'
|
||||||
|
default_detail = _('The max size of CSV is %d bytes')
|
||||||
|
|
||||||
|
|
||||||
class JMSCSVParser(BaseParser):
|
class JMSCSVParser(BaseParser):
|
||||||
"""
|
"""
|
||||||
Parses CSV file to serializer data
|
Parses CSV file to serializer data
|
||||||
"""
|
"""
|
||||||
|
CSV_UPLOAD_MAX_SIZE = 1024 * 1024 * 10
|
||||||
|
|
||||||
media_type = 'text/csv'
|
media_type = 'text/csv'
|
||||||
|
|
||||||
@@ -46,23 +55,31 @@ class JMSCSVParser(BaseParser):
|
|||||||
return fields_map
|
return fields_map
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_row(row):
|
def _replace_chinese_quot(str_):
|
||||||
|
trans_table = str.maketrans({
|
||||||
|
'“': '"',
|
||||||
|
'”': '"',
|
||||||
|
'‘': '"',
|
||||||
|
'’': '"',
|
||||||
|
'\'': '"'
|
||||||
|
})
|
||||||
|
return str_.translate(trans_table)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _process_row(cls, row):
|
||||||
"""
|
"""
|
||||||
构建json数据前的行处理
|
构建json数据前的行处理
|
||||||
"""
|
"""
|
||||||
_row = []
|
_row = []
|
||||||
|
|
||||||
for col in row:
|
for col in row:
|
||||||
# 列表转换
|
# 列表转换
|
||||||
if isinstance(col, str) and col.find("[") != -1 and col.find("]") != -1:
|
if isinstance(col, str) and col.startswith('[') and col.endswith(']'):
|
||||||
# 替换中文格式引号
|
col = cls._replace_chinese_quot(col)
|
||||||
col = col.replace("“", '"').replace("”", '"').\
|
|
||||||
replace("‘", '"').replace('’', '"').replace("'", '"')
|
|
||||||
col = json.loads(col)
|
col = json.loads(col)
|
||||||
# 字典转换
|
# 字典转换
|
||||||
if isinstance(col, str) and col.find("{") != -1 and col.find("}") != -1:
|
if isinstance(col, str) and col.startswith("{") and col.endswith("}"):
|
||||||
# 替换中文格式引号
|
col = cls._replace_chinese_quot(col)
|
||||||
col = col.replace("“", '"').replace("”", '"'). \
|
|
||||||
replace("‘", '"').replace('’', '"').replace("'", '"')
|
|
||||||
col = json.loads(col)
|
col = json.loads(col)
|
||||||
_row.append(col)
|
_row.append(col)
|
||||||
return _row
|
return _row
|
||||||
@@ -82,11 +99,19 @@ class JMSCSVParser(BaseParser):
|
|||||||
def parse(self, stream, media_type=None, parser_context=None):
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
parser_context = parser_context or {}
|
parser_context = parser_context or {}
|
||||||
try:
|
try:
|
||||||
serializer = parser_context["view"].get_serializer()
|
view = parser_context['view']
|
||||||
|
meta = view.request.META
|
||||||
|
serializer = view.get_serializer()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e, exc_info=True)
|
logger.debug(e, exc_info=True)
|
||||||
raise ParseError('The resource does not support imports!')
|
raise ParseError('The resource does not support imports!')
|
||||||
|
|
||||||
|
content_length = int(meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0)))
|
||||||
|
if content_length > self.CSV_UPLOAD_MAX_SIZE:
|
||||||
|
msg = CsvDataTooBig.default_detail % self.CSV_UPLOAD_MAX_SIZE
|
||||||
|
logger.error(msg)
|
||||||
|
raise CsvDataTooBig(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stream_data = stream.read()
|
stream_data = stream.read()
|
||||||
stream_data = stream_data.strip(codecs.BOM_UTF8)
|
stream_data = stream_data.strip(codecs.BOM_UTF8)
|
||||||
|
|||||||
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
|
||||||
|
class JMSException(APIException):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import time
|
|||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@@ -15,8 +16,8 @@ from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
|
|||||||
from ..utils import lazyproperty
|
from ..utils import lazyproperty
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"JSONResponseMixin", "CommonApiMixin",
|
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
|
||||||
'AsyncApiMixin', 'RelationMixin'
|
'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -54,9 +55,10 @@ class ExtraFilterFieldsMixin:
|
|||||||
def get_filter_backends(self):
|
def get_filter_backends(self):
|
||||||
if self.filter_backends != self.__class__.filter_backends:
|
if self.filter_backends != self.__class__.filter_backends:
|
||||||
return self.filter_backends
|
return self.filter_backends
|
||||||
backends = list(self.filter_backends) + \
|
backends = list(chain(
|
||||||
list(self.default_added_filters) + \
|
self.filter_backends,
|
||||||
list(self.extra_filter_backends)
|
self.default_added_filters,
|
||||||
|
self.extra_filter_backends))
|
||||||
return backends
|
return backends
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
@@ -233,3 +235,32 @@ class RelationMixin:
|
|||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
self.send_post_add_signal(instance)
|
self.send_post_add_signal(instance)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerMixin2:
|
||||||
|
serializer_classes = {}
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.serializer_classes:
|
||||||
|
serializer_class = self.serializer_classes.get(
|
||||||
|
self.action, self.serializer_classes.get('default')
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(serializer_class, dict):
|
||||||
|
serializer_class = serializer_class.get(
|
||||||
|
self.request.method.lower, serializer_class.get('default')
|
||||||
|
)
|
||||||
|
|
||||||
|
assert serializer_class, '`serializer_classes` config error'
|
||||||
|
return serializer_class
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
|
||||||
|
class QuerySetMixin:
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
serializer_class = self.get_serializer_class()
|
||||||
|
if serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
|
||||||
|
queryset = serializer_class.setup_eager_loading(queryset)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ class Config(dict):
|
|||||||
'TERMINAL_COMMAND_STORAGE': {},
|
'TERMINAL_COMMAND_STORAGE': {},
|
||||||
|
|
||||||
'SECURITY_MFA_AUTH': False,
|
'SECURITY_MFA_AUTH': False,
|
||||||
|
'SECURITY_COMMAND_EXECUTION': True,
|
||||||
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
|
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
|
||||||
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
||||||
'SECURITY_LOGIN_LIMIT_COUNT': 7,
|
'SECURITY_LOGIN_LIMIT_COUNT': 7,
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework import status, generics
|
from rest_framework import status, generics
|
||||||
from rest_framework.views import Response
|
from rest_framework.views import Response
|
||||||
from rest_framework_bulk import BulkModelViewSet
|
from rest_framework_bulk import BulkModelViewSet
|
||||||
@@ -22,6 +23,8 @@ logger = get_logger(__file__)
|
|||||||
|
|
||||||
|
|
||||||
class OrgViewSet(BulkModelViewSet):
|
class OrgViewSet(BulkModelViewSet):
|
||||||
|
filter_fields = ('name',)
|
||||||
|
search_fields = ('name', 'comment')
|
||||||
queryset = Organization.objects.all()
|
queryset = Organization.objects.all()
|
||||||
serializer_class = OrgSerializer
|
serializer_class = OrgSerializer
|
||||||
permission_classes = (IsSuperUserOrAppUser,)
|
permission_classes = (IsSuperUserOrAppUser,)
|
||||||
@@ -51,10 +54,12 @@ class OrgViewSet(BulkModelViewSet):
|
|||||||
for model in models:
|
for model in models:
|
||||||
data = self.get_data_from_model(model)
|
data = self.get_data_from_model(model)
|
||||||
if data:
|
if data:
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
msg = _('Organization contains undeleted resources')
|
||||||
|
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
|
||||||
else:
|
else:
|
||||||
if str(current_org) == str(self.org):
|
if str(current_org) == str(self.org):
|
||||||
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
msg = _('The current organization cannot be deleted')
|
||||||
|
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
|
||||||
self.org.delete()
|
self.org.delete()
|
||||||
return Response({'msg': True}, status=status.HTTP_200_OK)
|
return Response({'msg': True}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,7 @@ def on_create_set_created_by(sender, instance=None, **kwargs):
|
|||||||
return
|
return
|
||||||
if hasattr(instance, 'created_by') and not instance.created_by:
|
if hasattr(instance, 'created_by') and not instance.created_by:
|
||||||
if current_request and current_request.user.is_authenticated:
|
if current_request and current_request.user.is_authenticated:
|
||||||
instance.created_by = current_request.user.name
|
user_name = current_request.user.name
|
||||||
|
if isinstance(user_name, str):
|
||||||
|
user_name = user_name[:30]
|
||||||
|
instance.created_by = user_name
|
||||||
|
|||||||
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
|
q = self.request.query_params
|
||||||
multi_command_storage = get_multi_command_storage()
|
multi_command_storage = get_multi_command_storage()
|
||||||
queryset = multi_command_storage.filter(
|
queryset = multi_command_storage.filter(
|
||||||
date_from=date_from, date_to=date_to, input=q.get("input"),
|
date_from=date_from, date_to=date_to,
|
||||||
user=q.get("user"), asset=q.get("asset"),
|
user=q.get("user"), asset=q.get("asset"), system_user=q.get("system_user"),
|
||||||
system_user=q.get("system_user"),
|
input=q.get("input"), session=q.get("session_id"),
|
||||||
risk_level=self.get_query_risk_level(), org_id=self.get_org_id(),
|
risk_level=self.get_query_risk_level(), org_id=self.get_org_id(),
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
# 解决es存储命令时,父类根据filter_fields过滤出现异常的问题,返回的queryset类型list
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_filter_fields(self, request):
|
|
||||||
fields = self.filter_fields
|
|
||||||
fields.extend(["date_from", "date_to"])
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def get_date_range(self):
|
def get_date_range(self):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
days_ago = now - timezone.timedelta(days=self.default_days_ago)
|
days_ago = now - timezone.timedelta(days=self.default_days_ago)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class SessionViewSet(OrgBulkModelViewSet):
|
|||||||
permission_classes = (IsOrgAdminOrAppUser, )
|
permission_classes = (IsOrgAdminOrAppUser, )
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
"user", "asset", "system_user", "remote_addr",
|
"user", "asset", "system_user", "remote_addr",
|
||||||
"protocol", "terminal", "is_finished",
|
"protocol", "terminal", "is_finished", 'login_from',
|
||||||
]
|
]
|
||||||
date_range_filter_fields = [
|
date_range_filter_fields = [
|
||||||
('date_start', ('date_from', 'date_to'))
|
('date_start', ('date_from', 'date_to'))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class CommandBase(object):
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def filter(self, date_from=None, date_to=None,
|
def filter(self, date_from=None, date_to=None,
|
||||||
user=None, asset=None, system_user=None,
|
user=None, asset=None, system_user=None,
|
||||||
input=None, session=None):
|
input=None, session=None, risk_level=None, org_id=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
|||||||
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)
|
asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
|
||||||
system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True)
|
system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True)
|
||||||
system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
|
system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
|
||||||
login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST")
|
login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST", verbose_name=_("Login from"))
|
||||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||||
is_success = models.BooleanField(default=True, db_index=True)
|
is_success = models.BooleanField(default=True, db_index=True)
|
||||||
is_finished = models.BooleanField(default=False, db_index=True)
|
is_finished = models.BooleanField(default=False, db_index=True)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from .ticket import *
|
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_GENERAL = 'general'
|
||||||
TYPE_LOGIN_CONFIRM = 'login_confirm'
|
TYPE_LOGIN_CONFIRM = 'login_confirm'
|
||||||
|
TYPE_REQUEST_ASSET_PERM = 'request_asset'
|
||||||
TYPE_CHOICES = (
|
TYPE_CHOICES = (
|
||||||
(TYPE_GENERAL, _("General")),
|
(TYPE_GENERAL, _("General")),
|
||||||
(TYPE_LOGIN_CONFIRM, _("Login confirm"))
|
(TYPE_LOGIN_CONFIRM, _("Login confirm")),
|
||||||
|
(TYPE_REQUEST_ASSET_PERM, _('Request asset permission'))
|
||||||
)
|
)
|
||||||
ACTION_APPROVE = 'approve'
|
ACTION_APPROVE = 'approve'
|
||||||
ACTION_REJECT = 'reject'
|
ACTION_REJECT = 'reject'
|
||||||
|
|||||||
@@ -4,3 +4,6 @@
|
|||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class IsAssignee(BasePermission):
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return obj.is_assignee(request.user)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from .ticket import *
|
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'
|
app_name = 'tickets'
|
||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
|
|
||||||
|
# router.register('tickets/request-asset-perm', api.RequestAssetPermTicketViewSet, 'ticket-request-asset-perm')
|
||||||
router.register('tickets', api.TicketViewSet, 'ticket')
|
router.register('tickets', api.TicketViewSet, 'ticket')
|
||||||
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
|
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import uuid
|
|||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from common.permissions import (
|
from common.permissions import (
|
||||||
IsCurrentUserOrReadOnly
|
IsCurrentUserOrReadOnly
|
||||||
@@ -64,8 +65,9 @@ class UserProfileApi(generics.RetrieveUpdateAPIView):
|
|||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
age = request.session.get_expiry_age()
|
if not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE:
|
||||||
request.session.set_expiry(age)
|
age = request.session.get_expiry_age()
|
||||||
|
request.session.set_expiry(age)
|
||||||
return super().retrieve(request, *args, **kwargs)
|
return super().retrieve(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ..serializers import UserSerializer, UserRetrieveSerializer
|
|||||||
from .mixins import UserQuerysetMixin
|
from .mixins import UserQuerysetMixin
|
||||||
from ..models import User
|
from ..models import User
|
||||||
from ..signals import post_user_create
|
from ..signals import post_user_create
|
||||||
|
from ..filters import OrgRoleUserFilterBackend
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -35,6 +36,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
|
|||||||
'default': UserSerializer,
|
'default': UserSerializer,
|
||||||
'retrieve': UserRetrieveSerializer
|
'retrieve': UserRetrieveSerializer
|
||||||
}
|
}
|
||||||
|
extra_filter_backends = [OrgRoleUserFilterBackend]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().prefetch_related('groups')
|
return super().get_queryset().prefetch_related('groups')
|
||||||
|
|||||||
32
apps/users/filters.py
Normal file
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):
|
def set_fields_queryset(self):
|
||||||
users_field = self.fields.get('users')
|
users_field = self.fields.get('users')
|
||||||
if users_field:
|
if users_field:
|
||||||
users_field.child_relation.queryset = utils.get_current_org_members(exclude=('Auditor',))
|
users_field.child_relation.queryset = utils.get_current_org_members()
|
||||||
|
|
||||||
def validate_users(self, users):
|
|
||||||
for user in users:
|
|
||||||
if user.is_super_auditor:
|
|
||||||
msg = _('Auditors cannot be join in the user group')
|
|
||||||
raise serializers.ValidationError(msg)
|
|
||||||
return users
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_eager_loading(cls, queryset):
|
def setup_eager_loading(cls, queryset):
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
|
|||||||
if not role:
|
if not role:
|
||||||
return
|
return
|
||||||
choices = role._choices
|
choices = role._choices
|
||||||
choices.pop('App', None)
|
choices.pop(User.ROLE_APP, None)
|
||||||
|
request = self.context.get('request')
|
||||||
|
if request and hasattr(request, 'user') and not request.user.is_superuser:
|
||||||
|
choices.pop(User.ROLE_ADMIN, None)
|
||||||
|
choices.pop(User.ROLE_AUDITOR, None)
|
||||||
role._choices = choices
|
role._choices = choices
|
||||||
|
|
||||||
def validate_role(self, value):
|
def validate_role(self, value):
|
||||||
@@ -320,3 +324,9 @@ class UserUpdatePublicKeySerializer(serializers.ModelSerializer):
|
|||||||
new_public_key = self.validated_data.get('public_key')
|
new_public_key = self.validated_data.get('public_key')
|
||||||
instance.set_public_key(new_public_key)
|
instance.set_public_key(new_public_key)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class MiniUserSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'name', 'username']
|
||||||
|
|||||||
2
jms
2
jms
@@ -217,7 +217,7 @@ def get_start_celery_ansible_kwargs():
|
|||||||
|
|
||||||
def get_start_celery_default_kwargs():
|
def get_start_celery_default_kwargs():
|
||||||
print("\n- Start Celery as Distributed Task Queue: Celery")
|
print("\n- Start Celery as Distributed Task Queue: Celery")
|
||||||
return get_start_worker_kwargs('celery', 2)
|
return get_start_worker_kwargs('celery', 4)
|
||||||
|
|
||||||
|
|
||||||
def get_start_worker_kwargs(queue, num):
|
def get_start_worker_kwargs(queue, num):
|
||||||
|
|||||||
@@ -5,15 +5,19 @@ utils_dir=$(pwd)
|
|||||||
project_dir=$(dirname "$utils_dir")
|
project_dir=$(dirname "$utils_dir")
|
||||||
release_dir=${project_dir}/release
|
release_dir=${project_dir}/release
|
||||||
|
|
||||||
# 安装依赖包
|
|
||||||
command -v git || yum -y install git
|
|
||||||
|
|
||||||
# 打包
|
# 打包
|
||||||
cd "${project_dir}" || exit 3
|
cd "${project_dir}" || exit 3
|
||||||
rm -rf "${release_dir:?}/*"
|
rm -rf "${release_dir:?}"/*
|
||||||
to_dir="${release_dir}/jumpserver"
|
to_dir="${release_dir}/jumpserver"
|
||||||
mkdir -p "${to_dir}"
|
mkdir -p "${to_dir}"
|
||||||
git archive --format tar HEAD | tar x -C "${to_dir}"
|
|
||||||
|
if [[ -d '.git' ]];then
|
||||||
|
command -v git || yum -y install git
|
||||||
|
git archive --format tar HEAD | tar x -C "${to_dir}"
|
||||||
|
else
|
||||||
|
cp -R . /tmp/jumpserver
|
||||||
|
mv /tmp/jumpserver/* "${to_dir}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $(uname) == 'Darwin' ]];then
|
if [[ $(uname) == 'Darwin' ]];then
|
||||||
alias sedi="sed -i ''"
|
alias sedi="sed -i ''"
|
||||||
|
|||||||
Reference in New Issue
Block a user