feat: 解决冲突

This commit is contained in:
jiangweidong 2022-12-02 10:52:31 +08:00
commit 85aad7ba62
181 changed files with 6094 additions and 3598 deletions

3
.isort.cfg Normal file
View File

@ -0,0 +1,3 @@
[settings]
line_length=120
known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets

View File

@ -1,4 +1,15 @@
FROM python:3.8-slim as stage-build
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim FROM python:3.8-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com> MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \ ARG BUILD_DEPENDENCIES=" \
@ -11,6 +22,7 @@ ARG DEPENDENCIES=" \
freetds-dev \ freetds-dev \
libpq-dev \ libpq-dev \
libffi-dev \ libffi-dev \
libjpeg-dev \
libldap2-dev \ libldap2-dev \
libsasl2-dev \ libsasl2-dev \
libxml2-dev \ libxml2-dev \
@ -21,9 +33,9 @@ ARG DEPENDENCIES=" \
sshpass" sshpass"
ARG TOOLS=" \ ARG TOOLS=" \
ca-certificates \
curl \ curl \
default-mysql-client \ default-mysql-client \
iproute2 \
iputils-ping \ iputils-ping \
locales \ locales \
procps \ procps \
@ -33,33 +45,34 @@ ARG TOOLS=" \
unzip \ unzip \
wget" wget"
RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \ ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \ && apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \ && apt-get -y install --no-install-recommends ${TOOLS} \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& mkdir -p /root/.ssh/ \ && mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \ && echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \ && echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ARG TARGETARCH ARG DOWNLOAD_URL=https://download.jumpserver.org
ARG ORACLE_LIB_MAJOR=19
ARG ORACLE_LIB_MINOR=10
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
RUN mkdir -p /opt/oracle/ \ RUN mkdir -p /opt/oracle/ \
&& cd /opt/oracle/ \ && cd /opt/oracle/ \
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \ && wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \ && unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \ && sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \ && ldconfig \
&& rm -f ${ORACLE_FILE} && rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip
WORKDIR /tmp/build WORKDIR /tmp/build
COPY ./requirements ./requirements COPY ./requirements ./requirements
@ -68,27 +81,25 @@ ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip
ARG VERSION RUN --mount=type=cache,target=/root/.cache/pip \
ENV VERSION=$VERSION set -ex \
ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules && pip config set global.index-url ${PIP_MIRROR} \
ADD . . && pip install --upgrade pip \
RUN cd utils \ && pip install --upgrade setuptools wheel \
&& bash -ixeu build.sh \ && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& mv ../release/jumpserver /opt/jumpserver \ && pip install -r requirements/requirements.txt
&& rm -rf /tmp/build \
&& echo > /opt/jumpserver/config.yml COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN echo > /opt/jumpserver/config.yml \
&& rm -rf /tmp/build
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8 ENV LANG=zh_CN.UTF-8
ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules
EXPOSE 8070 EXPOSE 8070
EXPOSE 8080 EXPOSE 8080

96
Dockerfile.loong64 Normal file
View File

@ -0,0 +1,96 @@
FROM python:3.8-slim as stage-build
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libjpeg-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
ARG TOOLS=" \
ca-certificates \
curl \
default-mysql-client \
iputils-ping \
locales \
netcat \
redis-server \
telnet \
vim \
unzip \
wget"
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
set -ex \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /tmp/build
COPY ./requirements ./requirements
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
RUN --mount=type=cache,target=/root/.cache/pip \
set -ex \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools wheel \
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \
&& pip install $(grep 'PyNaCl' requirements/requirements.txt) \
&& GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install -r requirements/requirements.txt
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN echo > /opt/jumpserver/config.yml \
&& rm -rf /tmp/build
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@ -0,0 +1,52 @@
from rest_framework.views import APIView
from rest_framework import status
from django.http.response import JsonResponse
from django.utils.translation import ugettext_lazy as _
from common.drf.api import JMSBulkModelViewSet
from common.const.choices import ConnectMethodChoices
from ..models import ConnectACL
from .. import serializers
__all__ = ['ConnectACLViewSet', 'ConnectMethodsAPI', 'ConnectMethodPermissionsAPI']
class ConnectACLViewSet(JMSBulkModelViewSet):
queryset = ConnectACL.objects.all()
filterset_fields = ('name', )
search_fields = ('name',)
serializer_class = serializers.ConnectACLSerializer
class ConnectMethodsAPI(APIView):
rbac_perms = {
'GET': 'acls.view_connnectacl',
}
@staticmethod
def get(request, *args, **kwargs):
data = []
for m in ConnectMethodChoices.choices:
data.append({'label': m[1], 'value': m[0]})
return JsonResponse(data, safe=False)
class ConnectMethodPermissionsAPI(APIView):
rbac_perms = {
'GET': 'acls.view_connnectacl',
}
@staticmethod
def get(request, *args, **kwargs):
login_type = request.query_params.get('login_type')
if not login_type:
rules = ConnectACL().all_rules(request.user)
return JsonResponse({'rules': rules})
acl = ConnectACL.match(request.user, login_type)
if acl:
err = _('The current user is not allowed to login in this way')
return JsonResponse({'error': err})
else:
return JsonResponse({'msg': 'ok'})

View File

@ -0,0 +1,40 @@
# Generated by Django 3.2.16 on 2022-11-30 02:46
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('users', '0040_alter_user_source'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0003_auto_20211130_1037'),
]
operations = [
migrations.CreateModel(
name='ConnectACL',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('rules', models.JSONField(default=list, verbose_name='Rule')),
('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')),
('user_groups', models.ManyToManyField(blank=True, related_name='connect_acls', to='users.UserGroup', verbose_name='User group')),
('users', models.ManyToManyField(blank=True, related_name='connect_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Connect acl',
'ordering': ('priority', '-date_updated', 'name'),
},
),
]

View File

@ -0,0 +1,120 @@
from django.db import models
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from common.utils.connection import get_redis_client
from common.const.choices import ConnectMethodChoices
from orgs.mixins.models import OrgManager, OrgModelMixin
from .base import BaseACL, BaseACLQuerySet
class ACLManager(OrgManager):
def valid(self):
return self.get_queryset().valid()
class ConnectACL(BaseACL, OrgModelMixin):
ConnectACLUserCacheKey = 'CONNECT_ACL_USER_{}'
ConnectACLUserCacheTTL = 600
class ActionChoices(models.TextChoices):
reject = 'reject', _('Reject')
# 用户
users = models.ManyToManyField(
'users.User', related_name='connect_acls', blank=True,
verbose_name=_("User")
)
user_groups = models.ManyToManyField(
'users.UserGroup', related_name='connect_acls', blank=True,
verbose_name=_("User group"),
)
rules = models.JSONField(default=list, verbose_name=_('Rule'))
# 动作
action = models.CharField(
max_length=64, verbose_name=_('Action'),
choices=ActionChoices.choices, default=ActionChoices.reject
)
objects = ACLManager.from_queryset(BaseACLQuerySet)()
class Meta:
ordering = ('priority', '-date_updated', 'name')
verbose_name = _('Connect acl')
def __str__(self):
return self.name
@property
def rules_display(self):
return ', '.join(
[ConnectMethodChoices.get_label(i) for i in self.rules]
)
def is_action(self, action):
return self.action == action
@staticmethod
def match(user, connect_type):
if not user:
return
user_acls = user.connect_acls.all().valid().distinct()
for acl in user_acls:
if connect_type in acl.rules:
return acl
for user_group in user.groups.all():
acls = user_group.connect_acls.all().valid().distinct()
for acl in acls:
if connect_type in acl.rules:
return acl
def _get_all_rules_from_cache(self, user):
find = False
cache_key = self.ConnectACLUserCacheKey.format(user.id)
rules = cache.get(cache_key)
if rules is not None:
find = True
return rules, find
@staticmethod
def _get_all_rules_from_db(user):
connect_rules = set()
user_acls = user.connect_acls.all().valid()
user_acl_rules = user_acls.values_list('id', 'rules')
for r_id, rule in user_acl_rules:
connect_rules.update(rule)
for ug in user.groups.all():
user_group_acls = ug.connect_acls.all().valid()
user_group_rules = user_group_acls.values_list('id', 'rules')
for r_id, rule in user_group_rules:
connect_rules.update(rule)
return list(connect_rules)
def set_all_rules_to_cache(self, key, rules):
cache.set(key, rules, self.ConnectACLUserCacheTTL)
def all_rules(self, user):
rules, find = self._get_all_rules_from_cache(user)
if not find:
rules = self._get_all_rules_from_db(user)
self.set_all_rules_to_cache(
self.ConnectACLUserCacheKey.format(user.id), rules
)
return rules
def clear_rules_cache(self):
cache.delete_pattern(
self.ConnectACLUserCacheKey.format('*')
)
def save(self, *args, **kwargs):
self.clear_rules_cache()
return super().save(*args, **kwargs)
def delete(self, using=None, keep_parents=False):
self.clear_rules_cache()
return super().delete(using=using, keep_parents=keep_parents)

View File

@ -53,12 +53,13 @@ class LoginACL(BaseACL):
@staticmethod @staticmethod
def match(user, ip): def match(user, ip):
acls = LoginACL.filter_acl(user) acl_qs = LoginACL.filter_acl(user)
if not acls: if not acl_qs:
return return
for acl in acls: for acl in acl_qs:
if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists(): if acl.is_action(LoginACL.ActionChoices.confirm) and \
not acl.reviewers.exists():
continue continue
ip_group = acl.rules.get('ip_group') ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period') time_periods = acl.rules.get('time_period')
@ -79,12 +80,12 @@ class LoginACL(BaseACL):
login_datetime = local_now_display() login_datetime = local_now_display()
data = { data = {
'title': title, 'title': title,
'type': const.TicketType.login_confirm,
'applicant': self.user, 'applicant': self.user,
'apply_login_city': login_city,
'apply_login_ip': login_ip, 'apply_login_ip': login_ip,
'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID, 'org_id': Organization.ROOT_ID,
'apply_login_city': login_city,
'apply_login_datetime': login_datetime,
'type': const.TicketType.login_confirm,
} }
ticket = ApplyLoginTicket.objects.create(**data) ticket = ApplyLoginTicket.objects.create(**data)
assignees = self.reviewers.all() assignees = self.reviewers.all()

View File

@ -86,12 +86,12 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
title = _('Login asset confirm') + ' ({})'.format(user) title = _('Login asset confirm') + ' ({})'.format(user)
data = { data = {
'title': title, 'title': title,
'type': TicketType.login_asset_confirm, 'org_id': org_id,
'applicant': user, 'applicant': user,
'apply_login_user': user, 'apply_login_user': user,
'apply_login_asset': asset, 'apply_login_asset': asset,
'apply_login_account': str(account), 'apply_login_account': str(account),
'org_id': org_id, 'type': TicketType.login_asset_confirm,
} }
ticket = ApplyLoginAssetTicket.objects.create(**data) ticket = ApplyLoginAssetTicket.objects.create(**data)
ticket.open_by_system(assignees) ticket.open_by_system(assignees)

View File

@ -0,0 +1,36 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from common.const.choices import ConnectMethodChoices
from ..models import ConnectACL
__all__ = ['ConnectACLSerializer', ]
class ConnectACLSerializer(BulkModelSerializer):
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
class Meta:
model = ConnectACL
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'priority', 'rules', 'rules_display', 'action', 'action_display', 'is_active',
'date_created', 'date_updated', 'comment', 'created_by'
]
fields_m2m = ['users', 'user_groups']
fields = fields_small + fields_m2m
extra_kwargs = {
'priority': {'default': 50},
'is_active': {'default': True}
}
@staticmethod
def validate_rules(rules):
for r in rules:
label = ConnectMethodChoices.get_label(r)
if not label:
error = _('Invalid connection method: {}').format(r)
raise serializers.ValidationError(error)
return rules

View File

@ -2,38 +2,57 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer from common.drf.serializers import BulkModelSerializer
from common.drf.serializers import MethodSerializer from common.drf.serializers import MethodSerializer
from common.drf.fields import ObjectRelatedField
from jumpserver.utils import has_valid_xpack_license from jumpserver.utils import has_valid_xpack_license
from users.models import User
from ..models import LoginACL from ..models import LoginACL
from .rules import RuleSerializer from .rules import RuleSerializer
__all__ = ['LoginACLSerializer', ] __all__ = [
"LoginACLSerializer",
]
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') common_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
)
class LoginACLSerializer(BulkModelSerializer): class LoginACLSerializer(BulkModelSerializer):
user_display = serializers.ReadOnlyField(source='user.username', label=_('Username')) user = ObjectRelatedField(queryset=User.objects, label=_("User"))
reviewers_display = serializers.SerializerMethodField(label=_('Reviewers')) reviewers = ObjectRelatedField(
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) queryset=User.objects, label=_("Reviewers"), many=True, required=False
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') )
action_display = serializers.ReadOnlyField(
source="get_action_display", label=_("Action")
)
reviewers_amount = serializers.IntegerField(
read_only=True, source="reviewers.count"
)
rules = MethodSerializer() rules = MethodSerializer()
class Meta: class Meta:
model = LoginACL model = LoginACL
fields_mini = ['id', 'name'] fields_mini = ["id", "name"]
fields_small = fields_mini + [ fields_small = fields_mini + [
'priority', 'rules', 'action', 'action_display', "priority",
'is_active', 'user', 'user_display', "rules",
'date_created', 'date_updated', 'reviewers_amount', "action",
'comment', 'created_by' "action_display",
"is_active",
"user",
"date_created",
"date_updated",
"reviewers_amount",
"comment",
"created_by",
] ]
fields_fk = ['user', 'user_display'] fields_fk = ["user"]
fields_m2m = ['reviewers', 'reviewers_display'] fields_m2m = ["reviewers"]
fields = fields_small + fields_fk + fields_m2m fields = fields_small + fields_fk + fields_m2m
extra_kwargs = { extra_kwargs = {
'priority': {'default': 50}, "priority": {"default": 50},
'is_active': {'default': True}, "is_active": {"default": True},
"reviewers": {'allow_null': False, 'required': True}, "reviewers": {"allow_null": False, "required": True},
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -41,7 +60,7 @@ class LoginACLSerializer(BulkModelSerializer):
self.set_action_choices() self.set_action_choices()
def set_action_choices(self): def set_action_choices(self):
action = self.fields.get('action') action = self.fields.get("action")
if not action: if not action:
return return
choices = action._choices choices = action._choices
@ -51,6 +70,3 @@ class LoginACLSerializer(BulkModelSerializer):
def get_rules_serializer(self): def get_rules_serializer(self):
return RuleSerializer() return RuleSerializer()
def get_reviewers_display(self, obj):
return ','.join([str(user) for user in obj.reviewers.all()])

View File

@ -3,54 +3,66 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.models import Organization from orgs.models import Organization
from assets.const import Protocol from common.drf.fields import LabeledChoiceField
from acls import models from acls import models
__all__ = ['LoginAssetACLSerializer'] __all__ = ["LoginAssetACLSerializer"]
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ') common_help_text = _(
"Format for comma-delimited string, with * indicating a match all. "
)
class LoginAssetACLUsersSerializer(serializers.Serializer): class LoginAssetACLUsersSerializer(serializers.Serializer):
username_group = serializers.ListField( username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), default=["*"],
help_text=common_help_text child=serializers.CharField(max_length=128),
label=_("Username"),
help_text=common_help_text,
) )
class LoginAssetACLAssestsSerializer(serializers.Serializer): class LoginAssetACLAssestsSerializer(serializers.Serializer):
ip_group_help_text = _( ip_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. ' "Format for comma-delimited string, with * indicating a match all. "
'Such as: ' "Such as: "
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 ' "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 "
'(Domain name support)' "(Domain name support)"
) )
ip_group = serializers.ListField( ip_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'), default=["*"],
help_text=ip_group_help_text child=serializers.CharField(max_length=1024),
label=_("IP"),
help_text=ip_group_help_text,
) )
hostname_group = serializers.ListField( hostname_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'), default=["*"],
help_text=common_help_text child=serializers.CharField(max_length=128),
label=_("Hostname"),
help_text=common_help_text,
) )
class LoginAssetACLAccountsSerializer(serializers.Serializer): class LoginAssetACLAccountsSerializer(serializers.Serializer):
protocol_group_help_text = _( protocol_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. ' "Format for comma-delimited string, with * indicating a match all. "
'Protocol options: {}' "Protocol options: {}"
) )
name_group = serializers.ListField( name_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'), default=["*"],
help_text=common_help_text child=serializers.CharField(max_length=128),
label=_("Name"),
help_text=common_help_text,
) )
username_group = serializers.ListField( username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'), default=["*"],
help_text=common_help_text child=serializers.CharField(max_length=128),
label=_("Username"),
help_text=common_help_text,
) )
@ -58,34 +70,48 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
users = LoginAssetACLUsersSerializer() users = LoginAssetACLUsersSerializer()
assets = LoginAssetACLAssestsSerializer() assets = LoginAssetACLAssestsSerializer()
accounts = LoginAssetACLAccountsSerializer() accounts = LoginAssetACLAccountsSerializer()
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count') reviewers_amount = serializers.IntegerField(
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) read_only=True, source="reviewers.count"
)
action = LabeledChoiceField(
choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action")
)
class Meta: class Meta:
model = models.LoginAssetACL model = models.LoginAssetACL
fields_mini = ['id', 'name'] fields_mini = ["id", "name"]
fields_small = fields_mini + [ fields_small = fields_mini + [
'users', 'accounts', 'assets', "users",
'is_active', 'date_created', 'date_updated', "accounts",
'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id' "assets",
"is_active",
"date_created",
"date_updated",
"priority",
"action",
"comment",
"created_by",
"org_id",
] ]
fields_m2m = ['reviewers', 'reviewers_amount'] fields_m2m = ["reviewers", "reviewers_amount"]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
extra_kwargs = { extra_kwargs = {
"reviewers": {'allow_null': False, 'required': True}, "reviewers": {"allow_null": False, "required": True},
'priority': {'default': 50}, "priority": {"default": 50},
'is_active': {'default': True}, "is_active": {"default": True},
} }
def validate_reviewers(self, reviewers): def validate_reviewers(self, reviewers):
org_id = self.fields['org_id'].default() org_id = self.fields["org_id"].default()
org = Organization.get_instance(org_id) org = Organization.get_instance(org_id)
if not org: if not org:
error = _('The organization `{}` does not exist'.format(org_id)) error = _("The organization `{}` does not exist".format(org_id))
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
users = org.get_members() users = org.get_members()
valid_reviewers = list(set(reviewers) & set(users)) valid_reviewers = list(set(reviewers) & set(users))
if not valid_reviewers: if not valid_reviewers:
error = _('None of the reviewers belong to Organization `{}`'.format(org.name)) error = _(
"None of the reviewers belong to Organization `{}`".format(org.name)
)
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return valid_reviewers return valid_reviewers

View File

@ -1,89 +1,89 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import django_filters import django_filters
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from common.utils import get_logger from assets import serializers
from assets.models import Asset
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
from assets.tasks import (
push_accounts_to_assets, test_assets_connectivity_manual,
update_assets_hardware_info_manual, verify_accounts_connectivity,
)
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.mixins.api import SuggestionMixin from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet from common.utils import get_logger
from orgs.mixins import generics from orgs.mixins import generics
from assets import serializers from orgs.mixins.api import OrgBulkModelViewSet
from assets.models import Asset, Gateway
from assets.tasks import (
push_accounts_to_assets,
verify_accounts_connectivity,
test_assets_connectivity_manual,
update_assets_hardware_info_manual,
)
from assets.filters import NodeFilterBackend, LabelFilterBackend, IpInFilterBackend
from ..mixin import NodeFilterMixin from ..mixin import NodeFilterMixin
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AssetViewSet', 'AssetTaskCreateApi', 'AssetsTaskCreateApi', "AssetViewSet",
"AssetTaskCreateApi",
"AssetsTaskCreateApi",
] ]
class AssetFilterSet(BaseFilterSet): class AssetFilterSet(BaseFilterSet):
type = django_filters.CharFilter(field_name='platform__type', lookup_expr='exact') type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
category = django_filters.CharFilter(field_name='platform__category', lookup_expr='exact') category = django_filters.CharFilter(
hostname = django_filters.CharFilter(field_name='name', lookup_expr='exact') field_name="platform__category", lookup_expr="exact"
)
hostname = django_filters.CharFilter(field_name="name", lookup_expr="exact")
class Meta: class Meta:
model = Asset model = Asset
fields = ['name', 'address', 'is_active', 'type', 'category', 'hostname'] fields = ["name", "address", "is_active", "type", "category", "hostname"]
class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
""" """
API endpoint that allows Asset to be viewed or edited. API endpoint that allows Asset to be viewed or edited.
""" """
model = Asset model = Asset
filterset_class = AssetFilterSet filterset_class = AssetFilterSet
search_fields = ("name", "address") search_fields = ("name", "address")
ordering_fields = ("name", "address") ordering_fields = ("name", "address")
ordering = ('name',) ordering = ("name",)
serializer_classes = ( serializer_classes = (
('default', serializers.AssetSerializer), ("default", serializers.AssetSerializer),
('suggestion', serializers.MiniAssetSerializer), ("suggestion", serializers.MiniAssetSerializer),
('platform', serializers.PlatformSerializer), ("platform", serializers.PlatformSerializer),
('gateways', serializers.GatewayWithAuthSerializer) ("gateways", serializers.GatewayWithAuthSerializer),
) )
rbac_perms = ( rbac_perms = (
('match', 'assets.match_asset'), ("match", "assets.match_asset"),
('platform', 'assets.view_platform'), ("platform", "assets.view_platform"),
('gateways', 'assets.view_gateway') ("gateways", "assets.view_gateway"),
) )
extra_filter_backends = [ extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
LabelFilterBackend,
IpInFilterBackend,
NodeFilterBackend
]
@action(methods=['GET'], detail=True, url_path='platform') @action(methods=["GET"], detail=True, url_path="platform")
def platform(self, *args, **kwargs): def platform(self, *args, **kwargs):
asset = self.get_object() asset = self.get_object()
serializer = self.get_serializer(asset.platform) serializer = self.get_serializer(asset.platform)
return Response(serializer.data) return Response(serializer.data)
@action(methods=['GET'], detail=True, url_path='gateways') @action(methods=["GET"], detail=True, url_path="gateways")
def gateways(self, *args, **kwargs): def gateways(self, *args, **kwargs):
asset = self.get_object() asset = self.get_object()
if not asset.domain: if not asset.domain:
gateways = Gateway.objects.none() gateways = Asset.objects.none()
else: else:
gateways = asset.domain.gateways.filter(protocol='ssh') gateways = asset.domain.gateways.filter(protocol="ssh")
return self.get_paginated_response_from_queryset(gateways) return self.get_paginated_response_from_queryset(gateways)
class AssetsTaskMixin: class AssetsTaskMixin:
def perform_assets_task(self, serializer): def perform_assets_task(self, serializer):
data = serializer.validated_data data = serializer.validated_data
assets = data.get('assets', []) assets = data.get("assets", [])
asset_ids = [asset.id for asset in assets] asset_ids = [asset.id for asset in assets]
if data['action'] == "refresh": if data["action"] == "refresh":
task = update_assets_hardware_info_manual.delay(asset_ids) task = update_assets_hardware_info_manual.delay(asset_ids)
else: else:
task = test_assets_connectivity_manual.delay(asset_ids) task = test_assets_connectivity_manual.delay(asset_ids)
@ -94,9 +94,9 @@ class AssetsTaskMixin:
self.set_task_to_serializer_data(serializer, task) self.set_task_to_serializer_data(serializer, task)
def set_task_to_serializer_data(self, serializer, task): def set_task_to_serializer_data(self, serializer, task):
data = getattr(serializer, '_data', {}) data = getattr(serializer, "_data", {})
data["task"] = task.id data["task"] = task.id
setattr(serializer, '_data', data) setattr(serializer, "_data", data)
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
@ -104,18 +104,18 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
serializer_class = serializers.AssetTaskSerializer serializer_class = serializers.AssetTaskSerializer
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
pk = self.kwargs.get('pk') pk = self.kwargs.get("pk")
request.data['asset'] = pk request.data["asset"] = pk
request.data['assets'] = [pk] request.data["assets"] = [pk]
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
def check_permissions(self, request): def check_permissions(self, request):
action = request.data.get('action') action = request.data.get("action")
action_perm_require = { action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo', "refresh": "assets.refresh_assethardwareinfo",
'push_account': 'assets.push_assetsystemuser', "push_account": "assets.push_assetsystemuser",
'test': 'assets.test_assetconnectivity', "test": "assets.test_assetconnectivity",
'test_account': 'assets.test_assetconnectivity' "test_account": "assets.test_assetconnectivity",
} }
perm_required = action_perm_require.get(action) perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required) has = self.request.user.has_perm(perm_required)
@ -126,19 +126,19 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
@staticmethod @staticmethod
def perform_asset_task(serializer): def perform_asset_task(serializer):
data = serializer.validated_data data = serializer.validated_data
if data['action'] not in ['push_system_user', 'test_system_user']: if data["action"] not in ["push_system_user", "test_system_user"]:
return return
asset = data['asset'] asset = data["asset"]
accounts = data.get('accounts') accounts = data.get("accounts")
if not accounts: if not accounts:
accounts = asset.accounts.all() accounts = asset.accounts.all()
asset_ids = [asset.id] asset_ids = [asset.id]
account_ids = accounts.values_list('id', flat=True) account_ids = accounts.values_list("id", flat=True)
if action == 'push_account': if action == "push_account":
task = push_accounts_to_assets.delay(account_ids, asset_ids) task = push_accounts_to_assets.delay(account_ids, asset_ids)
elif action == 'test_account': elif action == "test_account":
task = verify_accounts_connectivity.delay(account_ids, asset_ids) task = verify_accounts_connectivity.delay(account_ids, asset_ids)
else: else:
task = None task = None
@ -156,9 +156,9 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
serializer_class = serializers.AssetsTaskSerializer serializer_class = serializers.AssetsTaskSerializer
def check_permissions(self, request): def check_permissions(self, request):
action = request.data.get('action') action = request.data.get("action")
action_perm_require = { action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo', "refresh": "assets.refresh_assethardwareinfo",
} }
perm_required = action_perm_require.get(action) perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required) has = self.request.user.has_perm(perm_required)

View File

@ -1,4 +1,3 @@
from assets.models import Host from assets.models import Host
from assets.serializers import HostSerializer from assets.serializers import HostSerializer
from .asset import AssetViewSet from .asset import AssetViewSet

View File

@ -5,7 +5,6 @@ from rest_framework import status, mixins, viewsets
from orgs.mixins import generics from orgs.mixins import generics
from assets import serializers from assets import serializers
from assets.const import AutomationTypes
from assets.tasks import execute_automation from assets.tasks import execute_automation
from assets.models import BaseAutomation, AutomationExecution from assets.models import BaseAutomation, AutomationExecution
from common.const.choices import Trigger from common.const.choices import Trigger
@ -111,8 +110,7 @@ class AutomationExecutionViewSet(
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
automation = serializer.validated_data.get('automation') automation = serializer.validated_data.get('automation')
tp = serializer.validated_data.get('type') tp = serializer.validated_data.get('type')
model = AutomationTypes.get_type_model(tp)
task = execute_automation.delay( task = execute_automation.delay(
pid=automation.pk, trigger=Trigger.manual, model=model pid=automation.pk, trigger=Trigger.manual, tp=tp
) )
return Response({'task': task.id}, status=status.HTTP_201_CREATED) return Response({'task': task.id}, status=status.HTTP_201_CREATED)

View File

@ -36,5 +36,5 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
execution = get_object_or_none(AutomationExecution, pk=eid) execution = get_object_or_none(AutomationExecution, pk=eid)
if execution: if execution:
queryset = queryset.filter(execution=execution) queryset = queryset.filter(execution=execution)
queryset = queryset.order_by('is_success', '-date_start') queryset = queryset.order_by('-date_started')
return queryset return queryset

View File

@ -7,10 +7,9 @@ from rest_framework.serializers import ValidationError
from common.utils import get_logger from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Domain, Gateway from ..models import Domain, Host
from .. import serializers from .. import serializers
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
@ -30,21 +29,26 @@ class DomainViewSet(OrgBulkModelViewSet):
class GatewayViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet):
model = Gateway filterset_fields = ("domain__name", "name", "domain")
filterset_fields = ("domain__name", "name", "username", "domain") search_fields = ("domain__name",)
search_fields = ("domain__name", "name", "username", )
serializer_class = serializers.GatewaySerializer serializer_class = serializers.GatewaySerializer
def get_queryset(self):
queryset = Host.get_gateway_queryset()
return queryset
class GatewayTestConnectionApi(SingleObjectMixin, APIView): class GatewayTestConnectionApi(SingleObjectMixin, APIView):
queryset = Gateway.objects.all()
object = None
rbac_perms = { rbac_perms = {
'POST': 'assets.test_gateway' 'POST': 'assets.test_gateway'
} }
def get_queryset(self):
queryset = Host.get_gateway_queryset()
return queryset
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all()) self.object = self.get_object()
local_port = self.request.data.get('port') or self.object.port local_port = self.request.data.get('port') or self.object.port
try: try:
local_port = int(local_port) local_port = int(local_port)

View File

@ -1,3 +1,5 @@
from .base import *
from .host import *
from .types import * from .types import *
from .account import * from .account import *
from .protocol import * from .protocol import *

View File

@ -1,5 +1,7 @@
from .base import BaseType from .base import BaseType
GATEWAY_NAME = 'Gateway'
class HostTypes(BaseType): class HostTypes(BaseType):
LINUX = 'linux', 'Linux' LINUX = 'linux', 'Linux'
@ -67,7 +69,7 @@ class HostTypes(BaseType):
return { return {
cls.LINUX: [ cls.LINUX: [
{'name': 'Linux'}, {'name': 'Linux'},
{'name': 'Gateway'} {'name': GATEWAY_NAME}
], ],
cls.UNIX: [ cls.UNIX: [
{'name': 'Unix'}, {'name': 'Unix'},

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db.models import Q from django.db.models import Q
from django_filters import rest_framework as drf_filters
from rest_framework import filters from rest_framework import filters
from rest_framework.compat import coreapi, coreschema from rest_framework.compat import coreapi, coreschema
from django_filters import rest_framework as drf_filters
from assets.utils import get_node_from_request, is_query_node_all_assets
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from assets.utils import is_query_node_all_assets, get_node_from_request
from .models import Label, Node, Account from .models import Account, Label, Node
class AssetByNodeFilterBackend(filters.BaseFilterBackend): class AssetByNodeFilterBackend(filters.BaseFilterBackend):

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-11 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_changesecretrecord_asset'),
]
operations = [
migrations.AlterField(
model_name='automationexecution',
name='status',
field=models.CharField(default='pending', max_length=16, verbose_name='Status'),
),
]

View File

@ -0,0 +1,73 @@
# Generated by Django 3.2.13 on 2022-09-29 11:03
from django.db import migrations
from assets.const.host import GATEWAY_NAME
def _create_account_obj(secret, secret_type, gateway, asset, account_model):
return account_model(
asset=asset,
secret=secret,
org_id=gateway.org_id,
secret_type=secret_type,
username=gateway.username,
name=f'{gateway.name}-{secret_type}-{GATEWAY_NAME.lower()}',
)
def migrate_gateway_to_asset(apps, schema_editor):
db_alias = schema_editor.connection.alias
gateway_model = apps.get_model('assets', 'Gateway')
platform_model = apps.get_model('assets', 'Platform')
gateway_platform = platform_model.objects.using(db_alias).get(name=GATEWAY_NAME)
print('>>> migrate gateway to asset')
asset_dict = {}
host_model = apps.get_model('assets', 'Host')
asset_model = apps.get_model('assets', 'Asset')
protocol_model = apps.get_model('assets', 'Protocol')
gateways = gateway_model.objects.all()
for gateway in gateways:
comment = gateway.comment if gateway.comment else ''
data = {
'comment': comment,
'name': f'{gateway.name}-{GATEWAY_NAME.lower()}',
'address': gateway.ip,
'domain': gateway.domain,
'org_id': gateway.org_id,
'is_active': gateway.is_active,
'platform': gateway_platform,
}
asset = asset_model.objects.using(db_alias).create(**data)
asset_dict[gateway.id] = asset
protocol_model.objects.using(db_alias).create(name='ssh', port=gateway.port, asset=asset)
hosts = [host_model(asset_ptr=asset) for asset in asset_dict.values()]
host_model.objects.using(db_alias).bulk_create(hosts, ignore_conflicts=True)
print('>>> migrate gateway to account')
accounts = []
account_model = apps.get_model('assets', 'Account')
for gateway in gateways:
password = gateway.password
private_key = gateway.private_key
asset = asset_dict[gateway.id]
if password:
accounts.append(_create_account_obj(
password, 'password', gateway, asset, account_model
))
if private_key:
accounts.append(_create_account_obj(
private_key, 'ssh_key', gateway, asset, account_model
))
account_model.objects.using(db_alias).bulk_create(accounts)
class Migration(migrations.Migration):
dependencies = [
('assets', '0111_alter_automationexecution_status'),
]
operations = [
migrations.RunPython(migrate_gateway_to_asset),
]

View File

@ -3,7 +3,8 @@ from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from common.utils import lazyproperty from common.utils import lazyproperty
from .base import BaseAccount, AbsConnectivity
from .base import AbsConnectivity, BaseAccount
__all__ = ['Account', 'AccountTemplate'] __all__ = ['Account', 'AccountTemplate']
@ -40,9 +41,10 @@ class AccountHistoricalRecords(HistoricalRecords):
class Account(AbsConnectivity, BaseAccount): class Account(AbsConnectivity, BaseAccount):
class InnerAccount(models.TextChoices): class AliasAccount(models.TextChoices):
INPUT = '@INPUT', '@INPUT' ALL = '@ALL', _('All')
USER = '@USER', '@USER' INPUT = '@INPUT', _('Manual input')
USER = '@USER', _('Dynamic user')
asset = models.ForeignKey( asset = models.ForeignKey(
'assets.Asset', related_name='accounts', 'assets.Asset', related_name='accounts',
@ -76,14 +78,14 @@ class Account(AbsConnectivity, BaseAccount):
return '{}'.format(self.username) return '{}'.format(self.username)
@classmethod @classmethod
def get_input_account(cls): def get_manual_account(cls):
""" @INPUT 手动登录的账号(any) """ """ @INPUT 手动登录的账号(any) """
return cls(name=cls.InnerAccount.INPUT.value, username='') return cls(name=cls.AliasAccount.INPUT.label, username=cls.AliasAccount.INPUT.value, secret=None)
@classmethod @classmethod
def get_user_account(cls, username): def get_user_account(cls, username):
""" @USER 动态用户的账号(self) """ """ @USER 动态用户的账号(self) """
return cls(name=cls.InnerAccount.USER.value, username=username) return cls(name=cls.AliasAccount.USER.label, username=cls.AliasAccount.USER.value)
class AccountTemplate(BaseAccount): class AccountTemplate(BaseAccount):

View File

@ -2,8 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import logging
import uuid import uuid
import logging
from collections import defaultdict from collections import defaultdict
from django.db import models from django.db import models

View File

@ -1,6 +1,12 @@
from assets.const import Category from assets.const import GATEWAY_NAME
from .common import Asset from .common import Asset
class Host(Asset): class Host(Asset):
pass
@classmethod
def get_gateway_queryset(cls):
queryset = cls.objects.filter(
platform__name=GATEWAY_NAME
)
return queryset

View File

@ -47,7 +47,7 @@ class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
def get_register_task(self): def get_register_task(self):
name = f"automation_{self.type}_strategy_period_{str(self.id)[:8]}" name = f"automation_{self.type}_strategy_period_{str(self.id)[:8]}"
task = execute_automation.name task = execute_automation.name
args = (str(self.id), Trigger.timing, self._meta.model) args = (str(self.id), Trigger.timing, self.type)
kwargs = {} kwargs = {}
return name, task, args, kwargs return name, task, args, kwargs

View File

@ -65,3 +65,9 @@ class ChangeSecretRecord(JMSBaseModel):
def __str__(self): def __str__(self):
return self.account.__str__() return self.account.__str__()
@property
def timedelta(self):
if self.date_started and self.date_finished:
return self.date_finished - self.date_started
return None

View File

@ -6,10 +6,10 @@ import sshpubkeys
from hashlib import md5 from hashlib import md5
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django.utils import timezone
from django.db.models import QuerySet from django.db.models import QuerySet
from django.utils.translation import ugettext_lazy as _
from common.utils import ( from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger, ssh_key_string_to_obj, ssh_key_gen, get_logger,

View File

@ -1,22 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import socket
import uuid import uuid
import socket
import random import random
from django.core.cache import cache
import paramiko import paramiko
from django.db import models from django.db import models
from django.core.cache import cache
from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, lazyproperty
from common.db import fields from common.db import fields
from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from assets.models import Host
from .base import BaseAccount from .base import BaseAccount
from ..const import SecretType
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = ['Domain', 'Gateway'] __all__ = ['Domain', 'GatewayMixin']
class Domain(OrgModelMixin): class Domain(OrgModelMixin):
@ -33,12 +36,9 @@ class Domain(OrgModelMixin):
def __str__(self): def __str__(self):
return self.name return self.name
def has_gateway(self):
return self.gateway_set.filter(is_active=True).exists()
@lazyproperty @lazyproperty
def gateways(self): def gateways(self):
return self.gateway_set.filter(is_active=True) return Host.get_gateway_queryset().filter(domain=self, is_active=True)
def select_gateway(self): def select_gateway(self):
return self.random_gateway() return self.random_gateway()
@ -53,18 +53,141 @@ class Domain(OrgModelMixin):
return random.choice(self.gateways) return random.choice(self.gateways)
class Gateway(BaseAccount): class GatewayMixin:
UNCONNECTIVE_KEY_TMPL = 'asset_unconnective_gateway_{}' id: uuid.UUID
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}' port: int
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5 address: str
accounts: QuerySet
private_key_path: str
private_key_obj: paramiko.RSAKey
UNCONNECTED_KEY_TMPL = 'asset_unconnective_gateway_{}'
UNCONNECTED_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
def set_unconnected(self):
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id)
unconnected_silence_period = cache.get(
unconnected_silence_period_key, self.UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE
)
cache.set(unconnected_silence_period_key, unconnected_silence_period * 2)
cache.set(unconnected_key, unconnected_silence_period, unconnected_silence_period)
def set_connective(self):
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id)
cache.delete(unconnected_key)
cache.delete(unconnected_silence_period_key)
def get_is_unconnected(self):
unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id)
return cache.get(unconnected_key, False)
@property
def is_connective(self):
return not self.get_is_unconnected()
@is_connective.setter
def is_connective(self, value):
if value:
self.set_connective()
else:
self.set_unconnected()
def test_connective(self, local_port=None):
# TODO 走ansible runner
if local_port is None:
local_port = self.port
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(self.address, port=self.port,
username=self.username,
password=self.password,
pkey=self.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException,
paramiko.ChannelException,
paramiko.ssh_exception.NoValidConnectionsError,
socket.gaierror) as e:
err = str(e)
if err.startswith('[Errno None] Unable to connect to port'):
err = _('Unable to connect to port {port} on {address}')
err = err.format(port=self.port, ip=self.address)
elif err == 'Authentication failed.':
err = _('Authentication failed')
elif err == 'Connect failed':
err = _('Connect failed')
self.is_connective = False
return False, err
try:
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0)
)
client.connect("127.0.0.1", port=local_port,
username=self.username,
password=self.password,
key_filename=self.private_key_path,
sock=sock,
timeout=5)
except (paramiko.SSHException,
paramiko.ssh_exception.SSHException,
paramiko.ChannelException,
paramiko.AuthenticationException,
TimeoutError) as e:
err = getattr(e, 'text', str(e))
if err == 'Connect failed':
err = _('Connect failed')
self.is_connective = False
return False, err
finally:
client.close()
self.is_connective = True
return True, None
@lazyproperty
def username(self):
account = self.accounts.all().first()
if account:
return account.username
logger.error(f'Gateway {self} has no account')
return ''
def get_secret(self, secret_type):
account = self.accounts.filter(secret_type=secret_type).first()
if account:
return account.secret
logger.error(f'Gateway {self} has no {secret_type} account')
@lazyproperty
def password(self):
secret_type = SecretType.PASSWORD
return self.get_secret(secret_type)
@lazyproperty
def private_key(self):
secret_type = SecretType.SSH_KEY
return self.get_secret(secret_type)
class Gateway(BaseAccount):
class Protocol(models.TextChoices): class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH' ssh = 'ssh', 'SSH'
name = models.CharField(max_length=128, verbose_name='Name') name = models.CharField(max_length=128, verbose_name='Name')
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")) protocol = models.CharField(
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
)
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
@ -85,91 +208,3 @@ class Gateway(BaseAccount):
permissions = [ permissions = [
('test_gateway', _('Test gateway')) ('test_gateway', _('Test gateway'))
] ]
def set_unconnective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
unconnective_silence_period = cache.get(unconnective_silence_period_key,
self.UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE)
cache.set(unconnective_silence_period_key, unconnective_silence_period * 2)
cache.set(unconnective_key, unconnective_silence_period, unconnective_silence_period)
def set_connective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
cache.delete(unconnective_key)
cache.delete(unconnective_silence_period_key)
def get_is_unconnective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
return cache.get(unconnective_key, False)
@property
def is_connective(self):
return not self.get_is_unconnective()
@is_connective.setter
def is_connective(self, value):
if value:
self.set_connective()
else:
self.set_unconnective()
def test_connective(self, local_port=None):
if local_port is None:
local_port = self.port
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(self.ip, port=self.port,
username=self.username,
password=self.password,
pkey=self.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException,
paramiko.ChannelException,
paramiko.ssh_exception.NoValidConnectionsError,
socket.gaierror) as e:
err = str(e)
if err.startswith('[Errno None] Unable to connect to port'):
err = _('Unable to connect to port {port} on {address}')
err = err.format(port=self.port, ip=self.ip)
elif err == 'Authentication failed.':
err = _('Authentication failed')
elif err == 'Connect failed':
err = _('Connect failed')
self.is_connective = False
return False, err
try:
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0)
)
client.connect("127.0.0.1", port=local_port,
username=self.username,
password=self.password,
key_filename=self.private_key_file,
sock=sock,
timeout=5)
except (paramiko.SSHException,
paramiko.ssh_exception.SSHException,
paramiko.ChannelException,
paramiko.AuthenticationException,
TimeoutError) as e:
err = getattr(e, 'text', str(e))
if err == 'Connect failed':
err = _('Connect failed')
self.is_connective = False
return False, err
finally:
client.close()
self.is_connective = True
return True, None

View File

@ -15,7 +15,7 @@ from ...const import Category, AllTypes
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
] ]

View File

@ -42,6 +42,19 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
)}, )},
}} }}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_secret_type_choices()
def set_secret_type_choices(self):
secret_type = self.fields.get('secret_type')
if not secret_type:
return
choices = secret_type._choices
choices.pop(SecretType.ACCESS_KEY, None)
choices.pop(SecretType.TOKEN, None)
secret_type._choices = choices
def validate_password_rules(self, password_rules): def validate_password_rules(self, password_rules):
secret_type = self.initial_secret_type secret_type = self.initial_secret_type
if secret_type != SecretType.PASSWORD: if secret_type != SecretType.PASSWORD:
@ -93,8 +106,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ChangeSecretRecord model = ChangeSecretRecord
fields = [ fields = [
'id', 'asset', 'account', 'date_started', 'id', 'asset', 'account', 'date_started', 'date_finished',
'date_finished', 'is_success', 'error', 'execution', 'timedelta', 'is_success', 'error', 'execution',
] ]
read_only_fields = fields read_only_fields = fields

View File

@ -1,30 +1,33 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from rest_framework.generics import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.validators import alphanumeric
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import SecretReadableMixin from common.drf.serializers import SecretReadableMixin
from ..models import Domain, Gateway from common.drf.fields import ObjectRelatedField, EncryptedField
from .base import AuthValidateMixin from assets.const import SecretType
from ..models import Domain, Asset, Account
from ..serializers import HostSerializer
from .utils import validate_password_for_ansible, validate_ssh_key
class DomainSerializer(BulkOrgResourceModelSerializer): class DomainSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField(label=_('Assets amount')) asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count')) gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
assets = ObjectRelatedField(
many=True, required=False, queryset=Asset.objects, label=_('Asset')
)
class Meta: class Meta:
model = Domain model = Domain
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + ['comment']
'comment', 'date_created' fields_m2m = ['assets']
] read_only_fields = ['asset_count', 'gateway_count', 'date_created']
fields_m2m = [ fields = fields_small + fields_m2m + read_only_fields
'asset_count', 'assets', 'gateway_count',
]
fields = fields_small + fields_m2m
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
extra_kwargs = { extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')}, 'assets': {'required': False, 'label': _('Assets')},
} }
@ -35,32 +38,89 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
@staticmethod @staticmethod
def get_gateway_count(obj): def get_gateway_count(obj):
return obj.gateway_set.all().count() return obj.gateways.count()
class GatewaySerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): class GatewaySerializer(HostSerializer):
is_connective = serializers.BooleanField(required=False, label=_('Connectivity')) password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
validators=[validate_password_for_ansible], write_only=True
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True,
max_length=16384, write_only=True
)
passphrase = serializers.CharField(
label=_('Key password'), allow_blank=True, allow_null=True, required=False, write_only=True,
max_length=512,
)
username = serializers.CharField(
label=_('Username'), allow_blank=True, max_length=128, required=True,
)
class Meta: class Meta(HostSerializer.Meta):
model = Gateway fields = HostSerializer.Meta.fields + [
fields_mini = ['id', 'username'] 'username', 'password', 'private_key', 'passphrase'
fields_write_only = [
'password', 'private_key', 'public_key', 'passphrase'
] ]
fields_small = fields_mini + fields_write_only + [
'ip', 'port', 'protocol', def validate_private_key(self, secret):
'is_active', 'is_connective', if not secret:
'date_created', 'date_updated', return
'created_by', 'comment', passphrase = self.initial_data.get('passphrase')
] passphrase = passphrase if passphrase else None
fields_fk = ['domain'] validate_ssh_key(secret, passphrase)
fields = fields_small + fields_fk return secret
extra_kwargs = {
'username': {"validators": [alphanumeric]}, @staticmethod
'password': {'write_only': True}, def clean_auth_fields(validated_data):
'private_key': {"write_only": True}, username = validated_data.pop('username', None)
'public_key': {"write_only": True}, password = validated_data.pop('password', None)
private_key = validated_data.pop('private_key', None)
validated_data.pop('passphrase', None)
return username, password, private_key
@staticmethod
def create_accounts(instance, username, password, private_key):
account_name = f'{instance.name}-{_("Gateway")}'
account_data = {
'privileged': True,
'name': account_name,
'username': username,
'asset_id': instance.id,
'created_by': instance.created_by
} }
if password:
Account.objects.create(
**account_data, secret=password, secret_type=SecretType.PASSWORD
)
if private_key:
Account.objects.create(
**account_data, secret=private_key, secret_type=SecretType.SSH_KEY
)
@staticmethod
def update_accounts(instance, username, password, private_key):
accounts = instance.accounts.filter(username=username)
if password:
account = get_object_or_404(accounts, SecretType.PASSWORD)
account.secret = password
account.save()
if private_key:
account = get_object_or_404(accounts, SecretType.SSH_KEY)
account.secret = private_key
account.save()
def create(self, validated_data):
auth_fields = self.clean_auth_fields(validated_data)
instance = super().create(validated_data)
self.create_accounts(instance, *auth_fields)
return instance
def update(self, instance, validated_data):
auth_fields = self.clean_auth_fields(validated_data)
instance = super().update(instance, validated_data)
self.update_accounts(instance, *auth_fields)
return instance
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer): class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):

View File

@ -1,11 +0,0 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
class CategoryDisplayMixin(serializers.Serializer):
category_display = serializers.ReadOnlyField(
source='get_category_display', label=_("Category display")
)
type_display = serializers.ReadOnlyField(
source='get_type_display', label=_("Type display")
)

View File

@ -1,61 +1,75 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.drf.fields import LabeledChoiceField from common.drf.fields import LabeledChoiceField
from common.drf.serializers import WritableNestedModelSerializer from common.drf.serializers import WritableNestedModelSerializer
from ..models import Platform, PlatformProtocol, PlatformAutomation
from ..const import Category, AllTypes from ..const import Category, AllTypes
from ..models import Platform, PlatformProtocol, PlatformAutomation
__all__ = ['PlatformSerializer', 'PlatformOpsMethodSerializer'] __all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer"]
class ProtocolSettingSerializer(serializers.Serializer): class ProtocolSettingSerializer(serializers.Serializer):
SECURITY_CHOICES = [ SECURITY_CHOICES = [
('any', 'Any'), ("any", "Any"),
('rdp', 'RDP'), ("rdp", "RDP"),
('tls', 'TLS'), ("tls", "TLS"),
('nla', 'NLA'), ("nla", "NLA"),
] ]
# RDP # RDP
console = serializers.BooleanField(required=False) console = serializers.BooleanField(required=False)
security = serializers.ChoiceField(choices=SECURITY_CHOICES, default='any') security = serializers.ChoiceField(choices=SECURITY_CHOICES, default="any")
# SFTP # SFTP
sftp_enabled = serializers.BooleanField(default=True, label=_("SFTP enabled")) sftp_enabled = serializers.BooleanField(default=True, label=_("SFTP enabled"))
sftp_home = serializers.CharField(default='/tmp', label=_("SFTP home")) sftp_home = serializers.CharField(default="/tmp", label=_("SFTP home"))
# HTTP # HTTP
auto_fill = serializers.BooleanField(default=False, label=_("Auto fill")) auto_fill = serializers.BooleanField(default=False, label=_("Auto fill"))
username_selector = serializers.CharField(default='', allow_blank=True, label=_("Username selector")) username_selector = serializers.CharField(
password_selector = serializers.CharField(default='', allow_blank=True, label=_("Password selector")) default="", allow_blank=True, label=_("Username selector")
submit_selector = serializers.CharField(default='', allow_blank=True, label=_("Submit selector")) )
password_selector = serializers.CharField(
default="", allow_blank=True, label=_("Password selector")
)
submit_selector = serializers.CharField(
default="", allow_blank=True, label=_("Submit selector")
)
class PlatformAutomationSerializer(serializers.ModelSerializer): class PlatformAutomationSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = PlatformAutomation model = PlatformAutomation
fields = [ fields = [
'id', 'ansible_enabled', 'ansible_config', "id",
'ping_enabled', 'ping_method', "ansible_enabled",
'gather_facts_enabled', 'gather_facts_method', "ansible_config",
'push_account_enabled', 'push_account_method', "ping_enabled",
'change_secret_enabled', 'change_secret_method', "ping_method",
'verify_account_enabled', 'verify_account_method', "gather_facts_enabled",
'gather_accounts_enabled', 'gather_accounts_method', "gather_facts_method",
"push_account_enabled",
"push_account_method",
"change_secret_enabled",
"change_secret_method",
"verify_account_enabled",
"verify_account_method",
"gather_accounts_enabled",
"gather_accounts_method",
] ]
extra_kwargs = { extra_kwargs = {
'ping_enabled': {'label': '启用资产探测'}, "ping_enabled": {"label": "启用资产探测"},
'ping_method': {'label': '探测方式'}, "ping_method": {"label": "探测方式"},
'gather_facts_enabled': {'label': '启用收集信息'}, "gather_facts_enabled": {"label": "启用收集信息"},
'gather_facts_method': {'label': '收集信息方式'}, "gather_facts_method": {"label": "收集信息方式"},
'verify_account_enabled': {'label': '启用校验账号'}, "verify_account_enabled": {"label": "启用校验账号"},
'verify_account_method': {'label': '校验账号方式'}, "verify_account_method": {"label": "校验账号方式"},
'push_account_enabled': {'label': '启用推送账号'}, "push_account_enabled": {"label": "启用推送账号"},
'push_account_method': {'label': '推送账号方式'}, "push_account_method": {"label": "推送账号方式"},
'change_secret_enabled': {'label': '启用账号改密'}, "change_secret_enabled": {"label": "启用账号改密"},
'change_secret_method': {'label': '账号创建改密方式'}, "change_secret_method": {"label": "账号创建改密方式"},
'gather_accounts_enabled': {'label': '启用账号收集'}, "gather_accounts_enabled": {"label": "启用账号收集"},
'gather_accounts_method': {'label': '收集账号方式'}, "gather_accounts_method": {"label": "收集账号方式"},
} }
@ -66,42 +80,62 @@ class PlatformProtocolsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = PlatformProtocol model = PlatformProtocol
fields = [ fields = [
'id', 'name', 'port', 'primary', 'default', "id",
'required', 'secret_types', 'setting', "name",
"port",
"primary",
"default",
"required",
"secret_types",
"setting",
] ]
class PlatformSerializer(WritableNestedModelSerializer): class PlatformSerializer(WritableNestedModelSerializer):
charset = LabeledChoiceField(
choices=Platform.CharsetChoices.choices, label=_("Charset")
)
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type")) type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
category = LabeledChoiceField(choices=Category.choices, label=_("Category")) category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
protocols = PlatformProtocolsSerializer(label=_('Protocols'), many=True, required=False) protocols = PlatformProtocolsSerializer(
automation = PlatformAutomationSerializer(label=_('Automation'), required=False) label=_("Protocols"), many=True, required=False
)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
su_method = LabeledChoiceField( su_method = LabeledChoiceField(
choices=[('sudo', 'sudo su -'), ('su', 'su - ')], choices=[("sudo", "sudo su -"), ("su", "su - ")],
label='切换方式', required=False, default='sudo' label="切换方式",
required=False,
default="sudo",
) )
class Meta: class Meta:
model = Platform model = Platform
fields_mini = ['id', 'name', 'internal'] fields_mini = ["id", "name", "internal"]
fields_small = fields_mini + [ fields_small = fields_mini + [
'category', 'type', 'charset', "category",
"type",
"charset",
] ]
fields = fields_small + [ fields = fields_small + [
'protocols_enabled', 'protocols', 'domain_enabled', "protocols_enabled",
'su_enabled', 'su_method', 'automation', 'comment', "protocols",
"domain_enabled",
"su_enabled",
"su_method",
"automation",
"comment",
] ]
extra_kwargs = { extra_kwargs = {
'su_enabled': {'label': '启用切换账号'}, "su_enabled": {"label": "启用切换账号"},
'protocols_enabled': {'label': '启用协议'}, "protocols_enabled": {"label": "启用协议"},
'domain_enabled': {'label': "启用网域"}, "domain_enabled": {"label": "启用网域"},
'domain_default': {'label': "默认网域"}, "domain_default": {"label": "默认网域"},
} }
class PlatformOpsMethodSerializer(serializers.Serializer): class PlatformOpsMethodSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True) id = serializers.CharField(read_only=True)
name = serializers.CharField(max_length=50, label=_('Name')) name = serializers.CharField(max_length=50, label=_("Name"))
category = serializers.CharField(max_length=50, label=_('Category')) category = serializers.CharField(max_length=50, label=_("Category"))
type = serializers.ListSerializer(child=serializers.CharField()) type = serializers.ListSerializer(child=serializers.CharField())
method = serializers.CharField() method = serializers.CharField()

View File

@ -1,13 +1,16 @@
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_lazy as _
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from assets.const import AutomationTypes
logger = get_logger(__file__) logger = get_logger(__file__)
@shared_task(queue='ansible') @shared_task(queue='ansible', verbose_name=_('Execute automation'))
def execute_automation(pid, trigger, model): def execute_automation(pid, trigger, tp):
model = AutomationTypes.get_type_model(tp)
with tmp_to_root_org(): with tmp_to_root_org():
instance = get_object_or_none(model, pk=pid) instance = get_object_or_none(model, pk=pid)
if not instance: if not instance:

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_lazy as _
from common.utils import get_object_or_none, get_logger from common.utils import get_object_or_none, get_logger
from orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
@ -9,7 +10,7 @@ from assets.models import AccountBackupPlan
logger = get_logger(__file__) logger = get_logger(__file__)
@shared_task @shared_task(verbose_name=_('Execute account backup plan'))
def execute_account_backup_plan(pid, trigger): def execute_account_backup_plan(pid, trigger):
with tmp_to_root_org(): with tmp_to_root_org():
plan = get_object_or_none(AccountBackupPlan, pk=pid) plan = get_object_or_none(AccountBackupPlan, pk=pid)

View File

@ -1,6 +1,7 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop
from django.utils.translation import gettext_lazy as _
from orgs.utils import tmp_to_root_org, org_aware_func from orgs.utils import tmp_to_root_org, org_aware_func
from common.utils import get_logger from common.utils import get_logger
@ -24,7 +25,7 @@ def gather_asset_accounts_util(nodes, task_name):
instance.execute() instance.execute()
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Gather asset accounts'))
def gather_asset_accounts(node_ids, task_name=None): def gather_asset_accounts(node_ids, task_name=None):
if task_name is None: if task_name is None:
task_name = gettext_noop("Gather assets accounts") task_name = gettext_noop("Gather assets accounts")

View File

@ -2,6 +2,7 @@
# #
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import org_aware_func, tmp_to_root_org from orgs.utils import org_aware_func, tmp_to_root_org
@ -40,7 +41,7 @@ def update_assets_hardware_info_util(assets=None, nodes=None, task_name=None):
instance.execute() instance.execute()
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets'))
def update_assets_hardware_info_manual(asset_ids): def update_assets_hardware_info_manual(asset_ids):
from assets.models import Asset from assets.models import Asset
with tmp_to_root_org(): with tmp_to_root_org():
@ -49,7 +50,7 @@ def update_assets_hardware_info_manual(asset_ids):
update_assets_hardware_info_util(assets=assets, task_name=task_name) update_assets_hardware_info_util(assets=assets, task_name=task_name)
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets under a node'))
def update_node_assets_hardware_info_manual(node_id): def update_node_assets_hardware_info_manual(node_id):
from assets.models import Node from assets.models import Node
with tmp_to_root_org(): with tmp_to_root_org():

View File

@ -10,11 +10,10 @@ from common.utils.lock import AcquireFailed
from common.utils import get_logger from common.utils import get_logger
from common.const.crontab import CRONTAB_AT_AM_TWO from common.const.crontab import CRONTAB_AT_AM_TWO
logger = get_logger(__file__) logger = get_logger(__file__)
@shared_task @shared_task(verbose_name=_('Check the amount of assets under the node'))
def check_node_assets_amount_task(org_id=None): def check_node_assets_amount_task(org_id=None):
if org_id is None: if org_id is None:
orgs = Organization.objects.all() orgs = Organization.objects.all()
@ -32,6 +31,6 @@ def check_node_assets_amount_task(org_id=None):
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO) @register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
@shared_task @shared_task(verbose_name=_('Periodic check the amount of assets under the node'))
def check_node_assets_amount_period_task(): def check_node_assets_amount_period_task():
check_node_assets_amount_task() check_node_assets_amount_task()

View File

@ -1,6 +1,7 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import org_aware_func, tmp_to_root_org from orgs.utils import org_aware_func, tmp_to_root_org
@ -29,7 +30,7 @@ def test_asset_connectivity_util(assets, task_name=None):
instance.execute() instance.execute()
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of a asset'))
def test_assets_connectivity_manual(asset_ids): def test_assets_connectivity_manual(asset_ids):
from assets.models import Asset from assets.models import Asset
with tmp_to_root_org(): with tmp_to_root_org():
@ -39,7 +40,7 @@ def test_assets_connectivity_manual(asset_ids):
test_asset_connectivity_util(assets, task_name=task_name) test_asset_connectivity_util(assets, task_name=task_name)
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of assets under a node'))
def test_node_assets_connectivity_manual(node_id): def test_node_assets_connectivity_manual(node_id):
from assets.models import Node from assets.models import Node
with tmp_to_root_org(): with tmp_to_root_org():

View File

@ -3,6 +3,7 @@ from django.utils.translation import gettext_noop
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import org_aware_func, tmp_to_root_org from orgs.utils import org_aware_func, tmp_to_root_org
from django.utils.translation import ugettext_lazy as _
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
@ -27,7 +28,7 @@ def push_accounts_to_assets_util(accounts, assets):
instance.execute() instance.execute()
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Push accounts to assets'))
def push_accounts_to_assets(account_ids, asset_ids): def push_accounts_to_assets(account_ids, asset_ids):
from assets.models import Asset, Account from assets.models import Asset, Account
with tmp_to_root_org(): with tmp_to_root_org():

View File

@ -1,5 +1,6 @@
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop
from django.utils.translation import ugettext as _
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import org_aware_func, tmp_to_root_org from orgs.utils import org_aware_func, tmp_to_root_org
@ -26,7 +27,7 @@ def verify_accounts_connectivity_util(accounts, assets, task_name):
instance.execute() instance.execute()
@shared_task(queue="ansible") @shared_task(queue="ansible", verbose_name=_('Verify asset account availability'))
def verify_accounts_connectivity(account_ids, asset_ids): def verify_accounts_connectivity(account_ids, asset_ids):
from assets.models import Asset, Account from assets.models import Asset, Account
with tmp_to_root_org(): with tmp_to_root_org():

View File

@ -1,24 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import TextChoices, IntegerChoices
DEFAULT_CITY = _("Unknown") DEFAULT_CITY = _("Unknown")
MODELS_NEED_RECORD = ( MODELS_NEED_RECORD = (
# users # users
'User', 'UserGroup', "User",
"UserGroup",
# acls # acls
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting', "LoginACL",
"LoginAssetACL",
"LoginConfirmSetting",
# assets # assets
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', "Asset",
'CommandFilter', 'Platform', 'Account', "Node",
"AdminUser",
"SystemUser",
"Domain",
"Gateway",
"CommandFilterRule",
"CommandFilter",
"Platform",
"Account",
# applications # applications
# orgs # orgs
'Organization', "Organization",
# settings # settings
'Setting', "Setting",
# perms # perms
'AssetPermission', "AssetPermission",
# xpack # xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', "License",
"Account",
"SyncInstanceTask",
"ChangeAuthPlan",
"GatherUserTask",
) )
class OperateChoices(TextChoices):
mkdir = "mkdir", _("Mkdir")
rmdir = "rmdir", _("Rmdir")
delete = "delete", _("Delete")
upload = "upload", _("Upload")
rename = "rename", _("Rename")
symlink = "symlink", _("Symlink")
download = "download", _("Download")
class ActionChoices(TextChoices):
view = "view", _("View")
update = "update", _("Update")
delete = "delete", _("Delete")
create = "create", _("Create")
class LoginTypeChoices(TextChoices):
web = "W", _("Web")
terminal = "T", _("Terminal")
unknown = "U", _("Unknown")
class MFAChoices(IntegerChoices):
disabled = 0, _("Disabled")
enabled = 1, _("Enabled")
unknown = 2, _("-")
class LoginStatusChoices(IntegerChoices):
success = True, _("Success")
failed = False, _("Failed")

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.14 on 2022-11-11 11:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0014_auto_20220505_1902'),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='operate',
field=models.CharField(choices=[('mkdir', 'Mkdir'), ('rmdir', 'Rmdir'), ('delete', 'Delete'), ('upload', 'Upload'), ('rename', 'Rename'), ('symlink', 'Symlink'), ('download', 'Download')], max_length=16, verbose_name='Operate'),
),
migrations.AlterField(
model_name='operatelog',
name='action',
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create')], max_length=16, verbose_name='Action'),
),
migrations.AlterField(
model_name='userloginlog',
name='status',
field=models.BooleanField(choices=[(1, 'Success'), (0, 'Failed')], default=1, verbose_name='Status'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2022-11-30 07:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0015_auto_20221011_1745'),
]
operations = [
migrations.AlterField(
model_name='userloginlog',
name='type',
field=models.CharField(choices=[('web_cli', 'Web Client'), ('web_gui', 'Web GUI'), ('db_cli', 'DB Client'), ('db_gui', 'DB GUI'), ('rdp_cli', 'RDP Client'), ('rdp_file', 'RDP File'), ('ssh_cli', 'SSH Client'), ('web_sftp', 'Web SFTP')], max_length=128, verbose_name='Login type'),
),
]

View File

@ -8,63 +8,55 @@ from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, Organization from orgs.mixins.models import OrgModelMixin, Organization
from orgs.utils import current_org from orgs.utils import current_org
from .const import (
OperateChoices,
ActionChoices,
LoginTypeChoices,
MFAChoices,
LoginStatusChoices,
)
__all__ = [ __all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog', "FTPLog",
"OperateLog",
"PasswordChangeLog",
"UserLoginLog",
] ]
class FTPLog(OrgModelMixin): class FTPLog(OrgModelMixin):
OPERATE_DELETE = 'Delete'
OPERATE_UPLOAD = 'Upload'
OPERATE_DOWNLOAD = 'Download'
OPERATE_RMDIR = 'Rmdir'
OPERATE_RENAME = 'Rename'
OPERATE_MKDIR = 'Mkdir'
OPERATE_SYMLINK = 'Symlink'
OPERATE_CHOICES = (
(OPERATE_DELETE, _('Delete')),
(OPERATE_UPLOAD, _('Upload')),
(OPERATE_DOWNLOAD, _('Download')),
(OPERATE_RMDIR, _('Rmdir')),
(OPERATE_RENAME, _('Rename')),
(OPERATE_MKDIR, _('Mkdir')),
(OPERATE_SYMLINK, _('Symlink'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User')) user = models.CharField(max_length=128, verbose_name=_("User"))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(
max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
)
asset = models.CharField(max_length=1024, verbose_name=_("Asset")) asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
system_user = models.CharField(max_length=128, verbose_name=_("System user")) system_user = models.CharField(max_length=128, verbose_name=_("System user"))
operate = models.CharField(max_length=16, verbose_name=_("Operate"), choices=OPERATE_CHOICES) operate = models.CharField(
max_length=16, verbose_name=_("Operate"), choices=OperateChoices.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"))
class Meta: class Meta:
verbose_name = _("File transfer log") verbose_name = _("File transfer log")
class OperateLog(OrgModelMixin): class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
ACTION_VIEW = 'view'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
ACTION_CHOICES = (
(ACTION_CREATE, _("Create")),
(ACTION_VIEW, _("View")),
(ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete"))
)
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"))
action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action")) action = models.CharField(
max_length=16, choices=ActionChoices.choices, verbose_name=_("Action")
)
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
resource = models.CharField(max_length=128, verbose_name=_("Resource")) resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True) max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
)
datetime = models.DateTimeField(
auto_now=True, verbose_name=_("Datetime"), db_index=True
)
def __str__(self): def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource) return "<{}> {} <{}>".format(self.user, self.action, self.resource)
@ -84,50 +76,48 @@ class OperateLog(OrgModelMixin):
class PasswordChangeLog(models.Model): class PasswordChangeLog(models.Model):
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"))
change_by = models.CharField(max_length=128, verbose_name=_("Change by")) change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) remote_addr = models.CharField(
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime')) max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
)
datetime = models.DateTimeField(auto_now=True, verbose_name=_("Datetime"))
def __str__(self): def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user) return "{} change {}'s password".format(self.change_by, self.user)
class Meta: class Meta:
verbose_name = _('Password change log') verbose_name = _("Password change log")
class UserLoginLog(models.Model): class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
('U', 'Unknown'),
)
MFA_DISABLED = 0
MFA_ENABLED = 1
MFA_UNKNOWN = 2
MFA_CHOICE = (
(MFA_DISABLED, _('Disabled')),
(MFA_ENABLED, _('Enabled')),
(MFA_UNKNOWN, _('-')),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=128, verbose_name=_('Username')) username = models.CharField(max_length=128, verbose_name=_("Username"))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) type = models.CharField(
ip = models.GenericIPAddressField(verbose_name=_('Login ip')) choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type")
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city')) )
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent')) ip = models.GenericIPAddressField(verbose_name=_("Login ip"))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA')) city = models.CharField(
reason = models.CharField(default='', max_length=128, blank=True, verbose_name=_('Reason')) max_length=254, blank=True, null=True, verbose_name=_("Login city")
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status')) )
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login')) user_agent = models.CharField(
backend = models.CharField(max_length=32, default='', verbose_name=_('Authentication backend')) max_length=254, blank=True, null=True, verbose_name=_("User agent")
)
mfa = models.SmallIntegerField(
default=MFAChoices.unknown, choices=MFAChoices.choices, verbose_name=_("MFA")
)
reason = models.CharField(
default="", max_length=128, blank=True, verbose_name=_("Reason")
)
status = models.BooleanField(
default=LoginStatusChoices.success,
choices=LoginStatusChoices.choices,
verbose_name=_("Status"),
)
datetime = models.DateTimeField(default=timezone.now, verbose_name=_("Date login"))
backend = models.CharField(
max_length=32, default="", verbose_name=_("Authentication backend")
)
@property @property
def backend_display(self): def backend_display(self):
@ -137,8 +127,8 @@ class UserLoginLog(models.Model):
def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None): def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None):
login_logs = cls.objects.all() login_logs = cls.objects.all()
if date_from and date_to: if date_from and date_to:
date_from = "{} {}".format(date_from, '00:00:00') date_from = "{} {}".format(date_from, "00:00:00")
date_to = "{} {}".format(date_to, '23:59:59') date_to = "{} {}".format(date_to, "23:59:59")
login_logs = login_logs.filter( login_logs = login_logs.filter(
datetime__gte=date_from, datetime__lte=date_to datetime__gte=date_from, datetime__lte=date_to
) )
@ -146,18 +136,19 @@ class UserLoginLog(models.Model):
login_logs = login_logs.filter(username=user) login_logs = login_logs.filter(username=user)
if keyword: if keyword:
login_logs = login_logs.filter( login_logs = login_logs.filter(
Q(ip__contains=keyword) | Q(ip__contains=keyword)
Q(city__contains=keyword) | | Q(city__contains=keyword)
Q(username__contains=keyword) | Q(username__contains=keyword)
) )
if not current_org.is_root(): if not current_org.is_root():
username_list = current_org.get_members().values_list('username', flat=True) username_list = current_org.get_members().values_list("username", flat=True)
login_logs = login_logs.filter(username__in=username_list) login_logs = login_logs.filter(username__in=username_list)
return login_logs return login_logs
@property @property
def reason_display(self): def reason_display(self):
from authentication.errors import reason_choices, old_reason_choices from authentication.errors import reason_choices, old_reason_choices
reason = reason_choices.get(self.reason) reason = reason_choices.get(self.reason)
if reason: if reason:
return reason return reason
@ -165,5 +156,5 @@ class UserLoginLog(models.Model):
return reason return reason
class Meta: class Meta:
ordering = ['-datetime', 'username'] ordering = ["-datetime", "username"]
verbose_name = _('User login log') verbose_name = _("User login log")

View File

@ -3,77 +3,99 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import BulkSerializerMixin from common.drf.fields import LabeledChoiceField
from terminal.models import Session from terminal.models import Session
from . import models from . import models
from .const import (
ActionChoices,
OperateChoices,
MFAChoices,
LoginStatusChoices,
LoginTypeChoices,
)
class FTPLogSerializer(serializers.ModelSerializer): class FTPLogSerializer(serializers.ModelSerializer):
operate_display = serializers.ReadOnlyField(source='get_operate_display', label=_('Operate display')) operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate"))
class Meta: class Meta:
model = models.FTPLog model = models.FTPLog
fields_mini = ['id'] fields_mini = ["id"]
fields_small = fields_mini + [ fields_small = fields_mini + [
'user', 'remote_addr', 'asset', 'system_user', 'org_id', "user",
'operate', 'filename', 'operate_display', "remote_addr",
'is_success', "asset",
'date_start', "system_user",
"org_id",
"operate",
"filename",
"is_success",
"date_start",
] ]
fields = fields_small fields = fields_small
class UserLoginLogSerializer(serializers.ModelSerializer): class UserLoginLogSerializer(serializers.ModelSerializer):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) mfa = LabeledChoiceField(choices=MFAChoices.choices, label=_("MFA"))
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display')) type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type"))
mfa_display = serializers.ReadOnlyField(source='get_mfa_display', label=_('MFA display')) status = LabeledChoiceField(choices=LoginStatusChoices.choices, label=_("Status"))
class Meta: class Meta:
model = models.UserLoginLog model = models.UserLoginLog
fields_mini = ['id'] fields_mini = ["id"]
fields_small = fields_mini + [ fields_small = fields_mini + [
'username', 'type', 'type_display', 'ip', 'city', 'user_agent', "username",
'mfa', 'mfa_display', 'reason', 'reason_display', 'backend', 'backend_display', "type",
'status', 'status_display', "ip",
'datetime', "city",
"user_agent",
"mfa",
"reason",
"reason_display",
"backend",
"backend_display",
"status",
"datetime",
] ]
fields = fields_small fields = fields_small
extra_kwargs = { extra_kwargs = {
"user_agent": {'label': _('User agent')}, "user_agent": {"label": _("User agent")},
"reason_display": {'label': _('Reason display')}, "reason_display": {"label": _("Reason display")},
'backend_display': {'label': _('Authentication backend')} "backend_display": {"label": _("Authentication backend")},
} }
class OperateLogSerializer(serializers.ModelSerializer): class OperateLogSerializer(serializers.ModelSerializer):
action_display = serializers.CharField(source='get_action_display', label=_('Action')) action = LabeledChoiceField(choices=ActionChoices.choices, label=_("Action"))
class Meta: class Meta:
model = models.OperateLog model = models.OperateLog
fields_mini = ['id'] fields_mini = ["id"]
fields_small = fields_mini + [ fields_small = fields_mini + [
'user', 'action', 'action_display', "user",
'resource_type', 'resource_type_display', 'resource', "action",
'remote_addr', 'datetime', 'org_id' "resource_type",
"resource_type_display",
"resource",
"remote_addr",
"datetime",
"org_id",
] ]
fields = fields_small fields = fields_small
extra_kwargs = { extra_kwargs = {"resource_type_display": {"label": _("Resource Type")}}
'resource_type_display': {'label': _('Resource Type')}
}
class PasswordChangeLogSerializer(serializers.ModelSerializer): class PasswordChangeLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.PasswordChangeLog model = models.PasswordChangeLog
fields = ( fields = ("id", "user", "change_by", "remote_addr", "datetime")
'id', 'user', 'change_by', 'remote_addr', 'datetime'
)
class SessionAuditSerializer(serializers.ModelSerializer): class SessionAuditSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Session model = Session
fields = '__all__' fields = "__all__"
# #
# class CommandExecutionSerializer(serializers.ModelSerializer): # class CommandExecutionSerializer(serializers.ModelSerializer):

View File

@ -1,38 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from django.db.models.signals import (
post_save, m2m_changed, pre_delete
)
from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.dispatch import receiver
from django.utils import timezone, translation
from django.utils.functional import LazyObject from django.utils.functional import LazyObject
from django.contrib.auth import BACKEND_SESSION_KEY from django.contrib.auth import BACKEND_SESSION_KEY
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import translation from django.db.models.signals import post_save, m2m_changed, pre_delete
from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.renderers import JSONRenderer
from assets.models import Asset
from authentication.signals import post_auth_failed, post_auth_success
from authentication.utils import check_different_city_login_if_need
from jumpserver.utils import current_request
from users.models import User
from users.signals import post_user_change_password
from terminal.models import Session, Command
from .utils import write_login_log, create_operate_log
from . import models, serializers
from .models import OperateLog
from orgs.utils import current_org from orgs.utils import current_org
from perms.models import AssetPermission from perms.models import AssetPermission
from terminal.backends.command.serializers import SessionCommandSerializer from users.models import User
from users.signals import post_user_change_password
from assets.models import Asset
from jumpserver.utils import current_request
from authentication.signals import post_auth_failed, post_auth_success
from authentication.utils import check_different_city_login_if_need
from terminal.models import Session, Command
from terminal.serializers import SessionSerializer from terminal.serializers import SessionSerializer
from terminal.backends.command.serializers import SessionCommandSerializer
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
from common.utils import get_request_ip, get_logger, get_syslogger from common.utils import get_request_ip, get_logger, get_syslogger
from common.utils.encode import data_to_json from common.utils.encode import data_to_json
from . import models, serializers
from .const import ActionChoices
from .utils import write_login_log, create_operate_log
logger = get_logger(__name__) logger = get_logger(__name__)
sys_logger = get_syslogger(__name__) sys_logger = get_syslogger(__name__)
@ -46,14 +42,14 @@ class AuthBackendLabelMapping(LazyObject):
for source, backends in User.SOURCE_BACKEND_MAPPING.items(): for source, backends in User.SOURCE_BACKEND_MAPPING.items():
for backend in backends: for backend in backends:
backend_label_mapping[backend] = source.label backend_label_mapping[backend] = source.label
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _("SSH Key")
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _("Password")
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') backend_label_mapping[settings.AUTH_BACKEND_SSO] = _("SSO")
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token') backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token")
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom")
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu') backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu")
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk")
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token') backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token")
return backend_label_mapping return backend_label_mapping
def _setup(self): def _setup(self):
@ -65,41 +61,41 @@ AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
M2M_NEED_RECORD = { M2M_NEED_RECORD = {
User.groups.through.__name__: ( User.groups.through.__name__: (
_('User and Group'), _("User and Group"),
_('{User} JOINED {UserGroup}'), _("{User} JOINED {UserGroup}"),
_('{User} LEFT {UserGroup}') _("{User} LEFT {UserGroup}"),
), ),
Asset.nodes.through.__name__: ( Asset.nodes.through.__name__: (
_('Node and Asset'), _("Node and Asset"),
_('{Node} ADD {Asset}'), _("{Node} ADD {Asset}"),
_('{Node} REMOVE {Asset}') _("{Node} REMOVE {Asset}"),
), ),
AssetPermission.users.through.__name__: ( AssetPermission.users.through.__name__: (
_('User asset permissions'), _("User asset permissions"),
_('{AssetPermission} ADD {User}'), _("{AssetPermission} ADD {User}"),
_('{AssetPermission} REMOVE {User}'), _("{AssetPermission} REMOVE {User}"),
), ),
AssetPermission.user_groups.through.__name__: ( AssetPermission.user_groups.through.__name__: (
_('User group asset permissions'), _("User group asset permissions"),
_('{AssetPermission} ADD {UserGroup}'), _("{AssetPermission} ADD {UserGroup}"),
_('{AssetPermission} REMOVE {UserGroup}'), _("{AssetPermission} REMOVE {UserGroup}"),
), ),
AssetPermission.assets.through.__name__: ( AssetPermission.assets.through.__name__: (
_('Asset permission'), _("Asset permission"),
_('{AssetPermission} ADD {Asset}'), _("{AssetPermission} ADD {Asset}"),
_('{AssetPermission} REMOVE {Asset}'), _("{AssetPermission} REMOVE {Asset}"),
), ),
AssetPermission.nodes.through.__name__: ( AssetPermission.nodes.through.__name__: (
_('Node permission'), _("Node permission"),
_('{AssetPermission} ADD {Node}'), _("{AssetPermission} ADD {Node}"),
_('{AssetPermission} REMOVE {Node}'), _("{AssetPermission} REMOVE {Node}"),
), ),
} }
M2M_ACTION_MAPER = { M2M_ACTION_MAPER = {
POST_ADD: OperateLog.ACTION_CREATE, POST_ADD: ActionChoices.create,
POST_REMOVE: OperateLog.ACTION_DELETE, POST_REMOVE: ActionChoices.delete,
POST_CLEAR: OperateLog.ACTION_DELETE, POST_CLEAR: ActionChoices.delete,
} }
@ -117,12 +113,14 @@ def on_m2m_changed(sender, action, instance, model, pk_set, **kwargs):
org_id = current_org.id org_id = current_org.id
remote_addr = get_request_ip(current_request) remote_addr = get_request_ip(current_request)
user = str(user) user = str(user)
resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name] resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[
sender_name
]
action = M2M_ACTION_MAPER[action] action = M2M_ACTION_MAPER[action]
if action == OperateLog.ACTION_CREATE: if action == ActionChoices.create:
resource_tmpl = resource_tmpl_add resource_tmpl = resource_tmpl_add
elif action == OperateLog.ACTION_DELETE: elif action == ActionChoices.delete:
resource_tmpl = resource_tmpl_remove resource_tmpl = resource_tmpl_remove
else: else:
return return
@ -139,41 +137,53 @@ def on_m2m_changed(sender, action, instance, model, pk_set, **kwargs):
print("Instace name: ", instance_name, instance_value) print("Instace name: ", instance_name, instance_value)
for obj in objs: for obj in objs:
resource = resource_tmpl.format(**{ resource = resource_tmpl.format(
instance_name: instance_value, **{instance_name: instance_value, model_name: str(obj)}
model_name: str(obj) )[
})[:128] # `resource` 字段只有 128 个字符长 😔 :128
] # `resource` 字段只有 128 个字符长 😔
to_create.append(OperateLog( to_create.append(
user=user, action=action, resource_type=resource_type, models.OperateLog(
resource=resource, remote_addr=remote_addr, org_id=org_id user=user,
)) action=action,
OperateLog.objects.bulk_create(to_create) resource_type=resource_type,
resource=resource,
remote_addr=remote_addr,
org_id=org_id,
)
)
models.OperateLog.objects.bulk_create(to_create)
@receiver(post_save) @receiver(post_save)
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): def on_object_created_or_update(
sender, instance=None, created=False, update_fields=None, **kwargs
):
# last_login 改变是最后登录日期, 每次登录都会改变 # last_login 改变是最后登录日期, 每次登录都会改变
if instance._meta.object_name == 'User' and \ if (
update_fields and 'last_login' in update_fields: instance._meta.object_name == "User"
and update_fields
and "last_login" in update_fields
):
return return
if created: if created:
action = models.OperateLog.ACTION_CREATE action = ActionChoices.create
else: else:
action = models.OperateLog.ACTION_UPDATE action = ActionChoices.update
create_operate_log(action, sender, instance) create_operate_log(action, sender, instance)
@receiver(pre_delete) @receiver(pre_delete)
def on_object_delete(sender, instance=None, **kwargs): def on_object_delete(sender, instance=None, **kwargs):
create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) create_operate_log(ActionChoices.delete, sender, instance)
@receiver(post_user_change_password, sender=User) @receiver(post_user_change_password, sender=User)
def on_user_change_password(sender, user=None, **kwargs): def on_user_change_password(sender, user=None, **kwargs):
if not current_request: if not current_request:
remote_addr = '127.0.0.1' remote_addr = "127.0.0.1"
change_by = 'System' change_by = "System"
else: else:
remote_addr = get_request_ip(current_request) remote_addr = get_request_ip(current_request)
if not current_request.user.is_authenticated: if not current_request.user.is_authenticated:
@ -182,7 +192,8 @@ def on_user_change_password(sender, user=None, **kwargs):
change_by = str(current_request.user) change_by = str(current_request.user)
with transaction.atomic(): with transaction.atomic():
models.PasswordChangeLog.objects.create( models.PasswordChangeLog.objects.create(
user=str(user), change_by=change_by, user=str(user),
change_by=change_by,
remote_addr=remote_addr, remote_addr=remote_addr,
) )
@ -216,51 +227,52 @@ def on_audits_log_create(sender, instance=None, **kwargs):
def get_login_backend(request): def get_login_backend(request):
backend = request.session.get('auth_backend', '') or \ backend = request.session.get("auth_backend", "") or request.session.get(
request.session.get(BACKEND_SESSION_KEY, '') BACKEND_SESSION_KEY, ""
)
backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None) backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None)
if backend_label is None: if backend_label is None:
backend_label = '' backend_label = ""
return backend_label return backend_label
def generate_data(username, request, login_type=None): def generate_data(username, request, login_type=None):
user_agent = request.META.get('HTTP_USER_AGENT', '') user_agent = request.META.get("HTTP_USER_AGENT", "")
login_ip = get_request_ip(request) or '0.0.0.0' login_ip = get_request_ip(request) or "0.0.0.0"
if login_type is None and isinstance(request, Request): if login_type is None and isinstance(request, Request):
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') login_type = request.META.get("HTTP_X_JMS_LOGIN_TYPE", "U")
if login_type is None: if login_type is None:
login_type = 'W' login_type = "W"
with translation.override('en'): with translation.override("en"):
backend = str(get_login_backend(request)) backend = str(get_login_backend(request))
data = { data = {
'username': username, "username": username,
'ip': login_ip, "ip": login_ip,
'type': login_type, "type": login_type,
'user_agent': user_agent[0:254], "user_agent": user_agent[0:254],
'datetime': timezone.now(), "datetime": timezone.now(),
'backend': backend, "backend": backend,
} }
return data return data
@receiver(post_auth_success) @receiver(post_auth_success)
def on_user_auth_success(sender, user, request, login_type=None, **kwargs): def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
logger.debug('User login success: {}'.format(user.username)) logger.debug("User login success: {}".format(user.username))
check_different_city_login_if_need(user, request) check_different_city_login_if_need(user, request)
data = generate_data(user.username, request, login_type=login_type) data = generate_data(user.username, request, login_type=login_type)
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S") request.session["login_time"] = data["datetime"].strftime("%Y-%m-%d %H:%M:%S")
data.update({'mfa': int(user.mfa_enabled), 'status': True}) data.update({"mfa": int(user.mfa_enabled), "status": True})
write_login_log(**data) write_login_log(**data)
@receiver(post_auth_failed) @receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason='', **kwargs): def on_user_auth_failed(sender, username, request, reason="", **kwargs):
logger.debug('User login failed: {}'.format(username)) logger.debug("User login failed: {}".format(username))
data = generate_data(username, request) data = generate_data(username, request)
data.update({'reason': reason[:128], 'status': False}) data.update({"reason": reason[:128], "status": False})
write_login_log(**data) write_login_log(**data)

View File

@ -1,33 +1,32 @@
import os
import abc
import json
import time
import base64 import base64
import json
import os
import time
import urllib.parse import urllib.parse
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.request import Request from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import PermissionDenied
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response
from common.drf.api import JMSModelViewSet from common.drf.api import JMSModelViewSet
from common.http import is_true from common.http import is_true
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from perms.models import Action from orgs.utils import tmp_to_root_org
from perms.models import ActionChoices
from terminal.models import EndpointRule from terminal.models import EndpointRule
from ..models import ConnectionToken
from ..serializers import ( from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer, SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
) )
from ..models import ConnectionToken
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
# ExtraActionApiMixin
class RDPFileClientProtocolURLMixin: class RDPFileClientProtocolURLMixin:
request: Request request: Request
@ -70,8 +69,7 @@ class RDPFileClientProtocolURLMixin:
# 设置磁盘挂载 # 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect')) drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect: if drives_redirect:
actions = Action.choices_to_value(token.actions) if ActionChoices.contains(token.actions, ActionChoices.transfer()):
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
rdp_options['drivestoredirect:s'] = '*' rdp_options['drivestoredirect:s'] = '*'
# 设置全屏 # 设置全屏
@ -179,22 +177,10 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: callable get_serializer: callable
perform_create: callable perform_create: callable
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
self.check_token_permission(token)
serializer = self.get_serializer(instance=token)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs): def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token() token = self.create_connection_token()
self.check_token_permission(token) token.is_valid()
filename, content = self.get_rdp_file_info(token) filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename) filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream') response = HttpResponse(content, content_type='application/octet-stream')
@ -204,7 +190,7 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
@action(methods=['POST', 'GET'], detail=False, url_path='client-url') @action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs): def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token() token = self.create_connection_token()
self.check_token_permission(token) token.is_valid()
try: try:
protocol_data = self.get_client_protocol_data(token) protocol_data = self.get_client_protocol_data(token)
except ValueError as e: except ValueError as e:
@ -222,12 +208,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.expire() instance.expire()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@staticmethod
def check_token_permission(token: ConnectionToken):
is_valid, error = token.check_permission()
if not is_valid:
raise PermissionDenied(error)
def create_connection_token(self): def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data) serializer = self.get_serializer(data=data)
@ -257,6 +237,22 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken',
} }
@action(methods=['POST'], detail=False, url_path='secret')
def get_secret_detail(self, request, *args, **kwargs):
""" 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """
rbac_perm = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(rbac_perm):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
token.is_valid()
serializer = self.get_serializer(instance=token)
return Response(serializer.data, status=status.HTTP_200_OK)
def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org():
return super().dispatch(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user) return ConnectionToken.objects.filter(user=self.request.user)
@ -264,25 +260,36 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
return self.request.user return self.request.user
def perform_create(self, serializer): def perform_create(self, serializer):
user = self.get_user(serializer) self.validate_serializer(serializer)
asset = serializer.validated_data.get('asset') return super().perform_create(serializer)
account_username = serializer.validated_data.get('account_username')
self.validate_asset_permission(user, asset, account_username)
return super(ConnectionTokenViewSet, self).perform_create(serializer)
@staticmethod def validate_serializer(self, serializer):
def validate_asset_permission(user, asset, account_username):
from perms.utils.account import PermAccountUtil from perms.utils.account import PermAccountUtil
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
if not actions:
error = 'No actions'
raise PermissionDenied(error)
if expire_at < time.time():
error = 'Expired'
raise PermissionDenied(error)
data = serializer.validated_data
user = self.get_user(serializer)
asset = data.get('asset')
login = data.get('login')
data['org_id'] = asset.org_id
data['user'] = user
# SuperConnectionToken util = PermAccountUtil()
permed_account = util.validate_permission(user, asset, login)
if not permed_account or not permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
user, asset, login
)
raise PermissionDenied(msg)
if permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired')
if permed_account.has_secret:
data['secret'] = ''
if permed_account.username != '@INPUT':
data['username'] = ''
return permed_account
class SuperConnectionTokenViewSet(ConnectionTokenViewSet): class SuperConnectionTokenViewSet(ConnectionTokenViewSet):

View File

@ -16,7 +16,7 @@ def migrate_system_user_to_account(apps, schema_editor):
count += len(connection_tokens) count += len(connection_tokens)
updated = [] updated = []
for connection_token in connection_tokens: for connection_token in connection_tokens:
connection_token.account_username = connection_token.system_user.username connection_token.account = connection_token.system_user.username
updated.append(connection_token) updated.append(connection_token)
connection_token_model.objects.bulk_update(updated, ['account_username']) connection_token_model.objects.bulk_update(updated, ['account_username'])

View File

@ -0,0 +1,34 @@
# Generated by Django 3.2.14 on 2022-11-22 13:52
import common.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0013_connectiontoken_protocol'),
]
operations = [
migrations.RenameField(
model_name='connectiontoken',
old_name='account_username',
new_name='login'
),
migrations.AlterField(
model_name='connectiontoken',
name='login',
field=models.CharField(max_length=128, verbose_name='Login account'),
),
migrations.AddField(
model_name='connectiontoken',
name='username',
field=models.CharField(default='', max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='connectiontoken',
name='secret',
field=common.db.fields.EncryptCharField(default='', max_length=128, verbose_name='Secret'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-23 02:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0014_auto_20221122_2152'),
]
operations = [
migrations.AlterField(
model_name='connectiontoken',
name='login',
field=models.CharField(max_length=128, verbose_name='Login account'),
),
]

View File

@ -2,13 +2,15 @@ import time
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from orgs.mixins.models import OrgModelMixin
from django.db import models from django.db import models
from common.utils import lazyproperty from django.conf import settings
from rest_framework.exceptions import PermissionDenied
from orgs.mixins.models import OrgModelMixin
from common.utils import lazyproperty, pretty_string
from common.utils.timezone import as_current_tz from common.utils.timezone import as_current_tz
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
from common.db.fields import EncryptCharField
from assets.const import Protocol from assets.const import Protocol
@ -25,13 +27,14 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True, 'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True,
related_name='connection_tokens', verbose_name=_('Asset'), related_name='connection_tokens', verbose_name=_('Asset'),
) )
login = models.CharField(max_length=128, verbose_name=_("Login account"))
username = models.CharField(max_length=128, default='', verbose_name=_("Username"))
secret = EncryptCharField(max_length=64, default='', verbose_name=_("Secret"))
protocol = models.CharField( protocol = models.CharField(
choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol")
) )
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
account_username = models.CharField(max_length=128, default='', verbose_name=_("Account"))
secret = models.CharField(max_length=64, default='', verbose_name=_("Secret"))
date_expired = models.DateTimeField( date_expired = models.DateTimeField(
default=date_expired_default, verbose_name=_("Date expired") default=date_expired_default, verbose_name=_("Date expired")
) )
@ -43,10 +46,6 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
('view_connectiontokensecret', _('Can view connection token secret')) ('view_connectiontokensecret', _('Can view connection token secret'))
] ]
@property
def is_valid(self):
return not self.is_expired
@property @property
def is_expired(self): def is_expired(self):
return self.date_expired < timezone.now() return self.date_expired < timezone.now()
@ -59,9 +58,10 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
seconds = 0 seconds = 0
return int(seconds) return int(seconds)
@classmethod def save(self, *args, **kwargs):
def get_default_date_expired(cls): self.asset_display = pretty_string(self.asset, max_length=128)
return date_expired_default() self.user_display = pretty_string(self.user, max_length=128)
return super().save(*args, **kwargs)
def expire(self): def expire(self):
self.date_expired = timezone.now() self.date_expired = timezone.now()
@ -69,48 +69,74 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
def renewal(self): def renewal(self):
""" 续期 Token将来支持用户自定义创建 token 后,续期策略要修改 """ """ 续期 Token将来支持用户自定义创建 token 后,续期策略要修改 """
self.date_expired = self.get_default_date_expired() self.date_expired = date_expired_default()
self.save() self.save()
# actions 和 expired_at 在 check_valid() 中赋值 @lazyproperty
actions = expire_at = None def permed_account(self):
from perms.utils import PermAccountUtil
permed_account = PermAccountUtil().validate_permission(
self.user, self.asset, self.login
)
return permed_account
def check_permission(self): @lazyproperty
from perms.utils.account import PermAccountUtil def actions(self):
return self.permed_account.actions
@lazyproperty
def expire_at(self):
return self.permed_account.date_expired.timestamp()
def is_valid(self):
if self.is_expired: if self.is_expired:
is_valid = False
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
return is_valid, error raise PermissionDenied(error)
if not self.user or not self.user.is_valid: if not self.user or not self.user.is_valid:
is_valid = False
error = _('No user or invalid user') error = _('No user or invalid user')
return is_valid, error raise PermissionDenied(error)
if not self.asset or not self.asset.is_active: if not self.asset or not self.asset.is_active:
is_valid = False is_valid = False
error = _('No asset or inactive asset') error = _('No asset or inactive asset')
return is_valid, error return is_valid, error
if not self.account_username: if not self.login:
is_valid = False
error = _('No account') error = _('No account')
return is_valid, error raise PermissionDenied(error)
actions, expire_at = PermAccountUtil().validate_permission(
self.user, self.asset, self.account_username if not self.permed_account or not self.permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
self.user, self.asset, self.login
) )
if not actions or expire_at < time.time(): raise PermissionDenied(msg)
is_valid = False
error = _('User has no permission to access asset or permission expired') if self.permed_account.date_expired < timezone.now():
return is_valid, error raise PermissionDenied('Expired')
self.actions = actions return True
self.expire_at = expire_at
is_valid, error = True, '' @lazyproperty
return is_valid, error def platform(self):
return self.asset.platform
@lazyproperty @lazyproperty
def account(self): def account(self):
if not self.asset: if not self.asset:
return None return None
account = self.asset.accounts.filter(username=self.account_username).first()
return account account = self.asset.accounts.filter(name=self.login).first()
if self.login == '@INPUT' or not account:
return {
'name': self.login,
'username': self.username,
'secret_type': 'password',
'secret': self.secret
}
else:
return {
'name': account.name,
'username': account.username,
'secret_type': account.secret_type,
'secret': account.secret_type or self.secret
}
@lazyproperty @lazyproperty
def domain(self): def domain(self):

View File

@ -1,14 +1,12 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from assets.serializers import PlatformSerializer
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from assets.models import Asset, Domain, CommandFilterRule, Account, Platform
from authentication.models import ConnectionToken from authentication.models import ConnectionToken
from common.utils import pretty_string from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from common.utils.random import random_string from perms.serializers.permission import ActionChoicesField
from assets.models import Asset, Gateway, Domain, CommandFilterRule, Account
from users.models import User from users.models import User
from perms.serializers.permission import ActionsField
__all__ = [ __all__ = [
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
@ -17,23 +15,25 @@ __all__ = [
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
is_valid = serializers.BooleanField(read_only=True, label=_('Validity')) username = serializers.CharField(max_length=128, label=_("Input username"),
allow_null=True, allow_blank=True)
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
class Meta: class Meta:
model = ConnectionToken model = ConnectionToken
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'secret', 'account_username', 'date_expired', 'protocol', 'login', 'secret', 'username',
'date_created', 'date_updated', 'actions', 'date_expired', 'date_created',
'created_by', 'updated_by', 'org_id', 'org_name', 'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name',
] ]
fields_fk = [ fields_fk = [
'user', 'asset', 'user', 'asset',
] ]
read_only_fields = [ read_only_fields = [
# 普通 Token 不支持指定 user # 普通 Token 不支持指定 user
'user', 'is_valid', 'expire_time', 'user', 'expire_time',
'user_display', 'asset_display', 'user_display', 'asset_display',
] ]
fields = fields_small + fields_fk + read_only_fields fields = fields_small + fields_fk + read_only_fields
@ -46,32 +46,6 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
def get_user(self, attrs): def get_user(self, attrs):
return self.get_request_user() return self.get_request_user()
def validate(self, attrs):
fields_attrs = self.construct_internal_fields_attrs(attrs)
attrs.update(fields_attrs)
return attrs
def construct_internal_fields_attrs(self, attrs):
asset = attrs.get('asset') or ''
asset_display = pretty_string(str(asset), max_length=128)
user = self.get_user(attrs)
user_display = pretty_string(str(user), max_length=128)
secret = attrs.get('secret') or random_string(16)
date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired()
org_id = asset.org_id
if not isinstance(asset, Asset):
error = ''
raise serializers.ValidationError(error)
attrs = {
'user': user,
'secret': secret,
'user_display': user_display,
'asset_display': asset_display,
'date_expired': date_expired,
'org_id': org_id,
}
return attrs
class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer): class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta): class Meta(ConnectionTokenSerializer.Meta):
@ -86,7 +60,6 @@ class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer):
class SuperConnectionTokenSerializer(ConnectionTokenSerializer): class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta): class Meta(ConnectionTokenSerializer.Meta):
read_only_fields = [ read_only_fields = [
'validity', 'user_display', 'system_user_display', 'validity', 'user_display', 'system_user_display',
@ -104,6 +77,7 @@ class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
class ConnectionTokenUserSerializer(serializers.ModelSerializer): class ConnectionTokenUserSerializer(serializers.ModelSerializer):
""" User """ """ User """
class Meta: class Meta:
model = User model = User
fields = ['id', 'name', 'username', 'email'] fields = ['id', 'name', 'username', 'email']
@ -111,6 +85,7 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class ConnectionTokenAssetSerializer(serializers.ModelSerializer): class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
""" Asset """ """ Asset """
class Meta: class Meta:
model = Asset model = Asset
fields = ['id', 'name', 'address', 'protocols', 'org_id'] fields = ['id', 'name', 'address', 'protocols', 'org_id']
@ -118,18 +93,20 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenAccountSerializer(serializers.ModelSerializer): class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
""" Account """ """ Account """
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'id', 'name', 'username', 'secret_type', 'secret', 'version' 'name', 'username', 'secret_type', 'secret',
] ]
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer): class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
""" Gateway """ """ Gateway """
class Meta: class Meta:
model = Gateway model = Asset
fields = ['id', 'ip', 'port', 'username', 'password', 'private_key'] fields = ['id', 'address', 'port', 'username', 'password', 'private_key']
class ConnectionTokenDomainSerializer(serializers.ModelSerializer): class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
@ -143,6 +120,7 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer): class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
""" Command filter rule """ """ Command filter rule """
class Meta: class Meta:
model = CommandFilterRule model = CommandFilterRule
fields = [ fields = [
@ -151,21 +129,30 @@ class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
] ]
class ConnectionTokenPlatform(PlatformSerializer):
class Meta(PlatformSerializer.Meta):
model = Platform
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
names = [n for n in names if n not in ['automation']]
return names
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = ConnectionTokenUserSerializer(read_only=True) user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True)
platform = ConnectionTokenPlatform(read_only=True)
account = ConnectionTokenAccountSerializer(read_only=True) account = ConnectionTokenAccountSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True) gateway = ConnectionTokenGatewaySerializer(read_only=True)
domain = ConnectionTokenDomainSerializer(read_only=True) # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) actions = ActionChoicesField()
actions = ActionsField()
expire_at = serializers.IntegerField() expire_at = serializers.IntegerField()
class Meta: class Meta:
model = ConnectionToken model = ConnectionToken
fields = [ fields = [
'id', 'secret', 'id', 'secret', 'user', 'asset', 'account',
'user', 'asset', 'account_username', 'account', 'protocol', 'protocol', 'domain', 'gateway',
'domain', 'gateway', 'cmd_filter_rules', 'actions', 'expire_at', 'platform',
'actions', 'expire_at',
] ]

View File

@ -1,27 +1,28 @@
from urllib.parse import urlencode
from django.conf import settings
from django.db.utils import IntegrityError
from django.http.request import HttpRequest
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from urllib.parse import urlencode
from django.views import View from django.views import View
from django.conf import settings
from django.http.request import HttpRequest
from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny, IsAuthenticated
from authentication import errors
from authentication.const import ConfirmType
from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin
from common.permissions import UserConfirmation
from common.sdk.im.dingtalk import URL, DingTalk
from common.utils import FlashMessageUtil, get_logger
from common.utils.common import get_request_ip
from common.utils.django import get_object_or_none, reverse
from common.utils.random import random_string
from users.models import User from users.models import User
from users.views import UserVerifyPasswordView from users.views import UserVerifyPasswordView
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
from common.sdk.im.dingtalk import URL
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
from common.permissions import UserConfirmation
from authentication import errors
from authentication.mixins import AuthMixin
from authentication.const import ConfirmType
from common.sdk.im.dingtalk import DingTalk
from common.utils.common import get_request_ip
from authentication.notifications import OAuthBindMessage
from .mixins import METAMixin from .mixins import METAMixin
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -1,26 +1,27 @@
from urllib.parse import urlencode
from django.conf import settings
from django.db.utils import IntegrityError
from django.http.request import HttpRequest
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from urllib.parse import urlencode
from django.views import View from django.views import View
from django.conf import settings
from django.http.request import HttpRequest
from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny, IsAuthenticated
from users.models import User
from users.views import UserVerifyPasswordView
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
from common.permissions import UserConfirmation
from common.sdk.im.feishu import FeiShu, URL
from common.utils.common import get_request_ip
from authentication import errors from authentication import errors
from authentication.const import ConfirmType from authentication.const import ConfirmType
from authentication.mixins import AuthMixin from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage from authentication.notifications import OAuthBindMessage
from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin
from common.permissions import UserConfirmation
from common.sdk.im.feishu import URL, FeiShu
from common.utils import FlashMessageUtil, get_logger
from common.utils.common import get_request_ip
from common.utils.django import get_object_or_none, reverse
from common.utils.random import random_string
from users.models import User
from users.views import UserVerifyPasswordView
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -1,19 +1,32 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import json import json
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from common.utils import signer, crypto from common.utils import signer, crypto
__all__ = [ __all__ = [
'JsonMixin', 'JsonDictMixin', 'JsonListMixin', 'JsonTypeMixin', "JsonMixin",
'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField', "JsonDictMixin",
'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField', "JsonListMixin",
'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField', "JsonTypeMixin",
'EncryptJsonDictCharField', 'PortField' "JsonCharField",
"JsonTextField",
"JsonListCharField",
"JsonListTextField",
"JsonDictCharField",
"JsonDictTextField",
"EncryptCharField",
"EncryptTextField",
"EncryptMixin",
"EncryptJsonDictTextField",
"EncryptJsonDictCharField",
"PortField",
"BitChoices",
] ]
@ -114,7 +127,7 @@ class EncryptMixin:
""" """
def decrypt_from_signer(self, value): def decrypt_from_signer(self, value):
return signer.unsign(value) or '' return signer.unsign(value) or ""
def from_db_value(self, value, expression, connection, context=None): def from_db_value(self, value, expression, connection, context=None):
if not value: if not value:
@ -129,7 +142,7 @@ class EncryptMixin:
# 可能和Json mix所以要先解密再json # 可能和Json mix所以要先解密再json
sp = super() sp = super()
if hasattr(sp, 'from_db_value'): if hasattr(sp, "from_db_value"):
plain_value = sp.from_db_value(plain_value, expression, connection, context) plain_value = sp.from_db_value(plain_value, expression, connection, context)
return plain_value return plain_value
@ -139,7 +152,7 @@ class EncryptMixin:
# 先 json 再解密 # 先 json 再解密
sp = super() sp = super()
if hasattr(sp, 'get_prep_value'): if hasattr(sp, "get_prep_value"):
value = sp.get_prep_value(value) value = sp.get_prep_value(value)
value = force_text(value) value = force_text(value)
# 替换新的加密方式 # 替换新的加密方式
@ -153,12 +166,12 @@ class EncryptTextField(EncryptMixin, models.TextField):
class EncryptCharField(EncryptMixin, models.CharField): class EncryptCharField(EncryptMixin, models.CharField):
@staticmethod @staticmethod
def change_max_length(kwargs): def change_max_length(kwargs):
kwargs.setdefault('max_length', 1024) kwargs.setdefault("max_length", 1024)
max_length = kwargs.get('max_length') max_length = kwargs.get("max_length")
if max_length < 129: if max_length < 129:
max_length = 128 max_length = 128
max_length = max_length * 2 max_length = max_length * 2
kwargs['max_length'] = max_length kwargs["max_length"] = max_length
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.change_max_length(kwargs) self.change_max_length(kwargs)
@ -166,10 +179,10 @@ class EncryptCharField(EncryptMixin, models.CharField):
def deconstruct(self): def deconstruct(self):
name, path, args, kwargs = super().deconstruct() name, path, args, kwargs = super().deconstruct()
max_length = kwargs.pop('max_length') max_length = kwargs.pop("max_length")
if max_length > 255: if max_length > 255:
max_length = max_length // 2 max_length = max_length // 2
kwargs['max_length'] = max_length kwargs["max_length"] = max_length
return name, path, args, kwargs return name, path, args, kwargs
@ -183,10 +196,50 @@ class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
class PortField(models.IntegerField): class PortField(models.IntegerField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs.update({ kwargs.update(
'blank': False, {
'null': False, "blank": False,
'validators': [MinValueValidator(0), MaxValueValidator(65535)] "null": False,
}) "validators": [MinValueValidator(0), MaxValueValidator(65535)],
}
)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class BitChoices(models.IntegerChoices):
@classmethod
def branches(cls):
return [i for i in cls]
@classmethod
def is_tree(cls):
return False
@classmethod
def tree(cls):
if not cls.is_tree():
return []
root = [_("All"), cls.branches()]
return [cls.render_node(root)]
@classmethod
def render_node(cls, node):
if isinstance(node, BitChoices):
return {
"value": node.name,
"label": node.label,
}
else:
name, children = node
return {
"value": name,
"label": name,
"children": [cls.render_node(child) for child in children],
}
@classmethod
def all(cls):
value = 0
for c in cls:
value |= c.value
return value

View File

@ -1,17 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import six import six
from rest_framework.fields import ChoiceField
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.fields import ChoiceField, empty
from common.db.fields import BitChoices
from common.utils import decrypt_password from common.utils import decrypt_password
__all__ = [ __all__ = [
'ReadableHiddenField', 'EncryptedField', 'LabeledChoiceField', "ReadableHiddenField",
'ObjectRelatedField', "EncryptedField",
"LabeledChoiceField",
"ObjectRelatedField",
"BitChoicesField",
"TreeChoicesMixin"
] ]
@ -21,13 +25,14 @@ __all__ = [
class ReadableHiddenField(serializers.HiddenField): class ReadableHiddenField(serializers.HiddenField):
"""可读的 HiddenField""" """可读的 HiddenField"""
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.write_only = False self.write_only = False
def to_representation(self, value): def to_representation(self, value):
if hasattr(value, 'id'): if hasattr(value, "id"):
return getattr(value, 'id') return getattr(value, "id")
return value return value
@ -35,7 +40,7 @@ class EncryptedField(serializers.CharField):
def __init__(self, write_only=None, **kwargs): def __init__(self, write_only=None, **kwargs):
if write_only is None: if write_only is None:
write_only = True write_only = True
kwargs['write_only'] = write_only kwargs["write_only"] = write_only
super().__init__(**kwargs) super().__init__(**kwargs)
def to_internal_value(self, value): def to_internal_value(self, value):
@ -54,26 +59,26 @@ class LabeledChoiceField(ChoiceField):
if value is None: if value is None:
return value return value
return { return {
'value': value, "value": value,
'label': self.choice_mapper.get(six.text_type(value), value), "label": self.choice_mapper.get(six.text_type(value), value),
} }
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, dict): if isinstance(data, dict):
return data.get('value') return data.get("value")
return super(LabeledChoiceField, self).to_internal_value(data) return super(LabeledChoiceField, self).to_internal_value(data)
class ObjectRelatedField(serializers.RelatedField): class ObjectRelatedField(serializers.RelatedField):
default_error_messages = { default_error_messages = {
'required': _('This field is required.'), "required": _("This field is required."),
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), "does_not_exist": _('Invalid pk "{pk_value}" - object does not exist.'),
'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), "incorrect_type": _("Incorrect type. Expected pk value, received {data_type}."),
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.attrs = kwargs.pop('attrs', None) or ('id', 'name') self.attrs = kwargs.pop("attrs", None) or ("id", "name")
self.many = kwargs.get('many', False) self.many = kwargs.get("many", False)
super().__init__(**kwargs) super().__init__(**kwargs)
def to_representation(self, value): def to_representation(self, value):
@ -86,13 +91,79 @@ class ObjectRelatedField(serializers.RelatedField):
if not isinstance(data, dict): if not isinstance(data, dict):
pk = data pk = data
else: else:
pk = data.get('id') or data.get('pk') or data.get(self.attrs[0]) pk = data.get("id") or data.get("pk") or data.get(self.attrs[0])
queryset = self.get_queryset() queryset = self.get_queryset()
try: try:
if isinstance(data, bool): if isinstance(data, bool):
raise TypeError raise TypeError
return queryset.get(pk=pk) return queryset.get(pk=pk)
except ObjectDoesNotExist: except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=pk) self.fail("does_not_exist", pk_value=pk)
except (TypeError, ValueError): except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(pk).__name__) self.fail("incorrect_type", data_type=type(pk).__name__)
class TreeChoicesMixin:
tree = []
class BitChoicesField(TreeChoicesMixin, serializers.MultipleChoiceField):
"""
位字段
"""
def __init__(self, choice_cls, **kwargs):
assert issubclass(choice_cls, BitChoices)
choices = [(c.name, c.label) for c in choice_cls]
self.tree = choice_cls.tree()
self._choice_cls = choice_cls
super().__init__(choices=choices, **kwargs)
def to_representation(self, value):
if isinstance(value, list) and len(value) == 1:
# Swagger 会使用 field.choices.keys() 迭代传递进来
return [
{"value": c.name, "label": c.label}
for c in self._choice_cls
if c.name == value[0]
]
return [
{"value": c.name, "label": c.label}
for c in self._choice_cls
if c.value & value == c.value
]
def to_internal_value(self, data):
if not isinstance(data, list):
raise serializers.ValidationError(_("Invalid data type, should be list"))
value = 0
if not data:
return value
if isinstance(data[0], dict):
data = [d["value"] for d in data]
# 所有的
if "all" in data:
for c in self._choice_cls:
value |= c.value
return value
name_value_map = {c.name: c.value for c in self._choice_cls}
for name in data:
if name not in name_value_map:
raise serializers.ValidationError(_("Invalid choice: {}").format(name))
value |= name_value_map[name]
return value
def run_validation(self, data=empty):
"""
备注:
创建授权规则不包含 actions 字段时, 会使用默认值(AssetPermission 中设置),
会直接使用 ['connect', '...'] 等字段保存到数据库导致类型错误
这里将获取到的值再执行一下 to_internal_value 方法, 转化为内部值
"""
data = super().run_validation(data)
if isinstance(data, int):
return data
value = self.to_internal_value(data)
self.run_validators(value)
return value

View File

@ -2,28 +2,33 @@
# #
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import datetime import datetime
from itertools import chain from collections import OrderedDict
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404 from django.http import Http404
from django.utils.encoding import force_text from django.utils.encoding import force_text
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata
from rest_framework.request import clone_request from rest_framework.request import clone_request
from common.drf.fields import TreeChoicesMixin
class SimpleMetadataWithFilters(SimpleMetadata): class SimpleMetadataWithFilters(SimpleMetadata):
"""Override SimpleMetadata, adding info about filters""" """Override SimpleMetadata, adding info about filters"""
methods = {"PUT", "POST", "GET", "PATCH"} methods = {"PUT", "POST", "GET", "PATCH"}
attrs = [ attrs = [
'read_only', 'label', 'help_text', "read_only",
'min_length', 'max_length', "label",
'min_value', 'max_value', "write_only", "help_text",
"min_length",
"max_length",
"min_value",
"max_value",
"write_only",
] ]
def determine_actions(self, request, view): def determine_actions(self, request, view):
@ -32,18 +37,18 @@ class SimpleMetadataWithFilters(SimpleMetadata):
the fields that are accepted for 'PUT' and 'POST' methods. the fields that are accepted for 'PUT' and 'POST' methods.
""" """
actions = {} actions = {}
view.raw_action = getattr(view, 'action', None) view.raw_action = getattr(view, "action", None)
for method in self.methods & set(view.allowed_methods): for method in self.methods & set(view.allowed_methods):
if hasattr(view, 'action_map'): if hasattr(view, "action_map"):
view.action = view.action_map.get(method.lower(), view.action) view.action = view.action_map.get(method.lower(), view.action)
view.request = clone_request(request, method) view.request = clone_request(request, method)
try: try:
# Test global permissions # Test global permissions
if hasattr(view, 'check_permissions'): if hasattr(view, "check_permissions"):
view.check_permissions(view.request) view.check_permissions(view.request)
# Test object permissions # Test object permissions
if method == 'PUT' and hasattr(view, 'get_object'): if method == "PUT" and hasattr(view, "get_object"):
view.get_object() view.get_object()
except (exceptions.APIException, PermissionDenied, Http404): except (exceptions.APIException, PermissionDenied, Http404):
pass pass
@ -56,70 +61,86 @@ class SimpleMetadataWithFilters(SimpleMetadata):
view.request = request view.request = request
return actions return actions
def get_field_type(self, field):
"""
Given a field, return a string representing the type of the field.
"""
tp = self.label_lookup[field]
class_name = field.__class__.__name__
if class_name == "LabeledChoiceField":
tp = "labeled_choice"
elif class_name == "ObjectRelatedField":
tp = "object_related_field"
elif class_name == "ManyRelatedField":
child_relation_class_name = field.child_relation.__class__.__name__
if child_relation_class_name == "ObjectRelatedField":
tp = "m2m_related_field"
return tp
@staticmethod
def set_choices_field(field, field_info):
field_info["choices"] = [
{
"value": choice_value,
"label": force_text(choice_label, strings_only=True),
}
for choice_value, choice_label in dict(field.choices).items()
]
@staticmethod
def set_tree_field(field, field_info):
field_info["tree"] = field.tree
field_info["type"] = "tree"
def get_field_info(self, field): def get_field_info(self, field):
""" """
Given an instance of a serializer field, return a dictionary Given an instance of a serializer field, return a dictionary
of metadata about it. of metadata about it.
""" """
field_info = OrderedDict() field_info = OrderedDict()
field_info['type'] = self.label_lookup[field] field_info["type"] = self.get_field_type(field)
field_info['required'] = getattr(field, 'required', False) field_info["required"] = getattr(field, "required", False)
default = getattr(field, 'default', None) # Default value
default = getattr(field, "default", None)
if default is not None and default != empty: if default is not None and default != empty:
if isinstance(default, (str, int, bool, float, datetime.datetime, list)): if isinstance(default, (str, int, bool, float, datetime.datetime, list)):
field_info['default'] = default field_info["default"] = default
for attr in self.attrs: for attr in self.attrs:
value = getattr(field, attr, None) value = getattr(field, attr, None)
if value is not None and value != '': if value is not None and value != "":
field_info[attr] = force_text(value, strings_only=True) field_info[attr] = force_text(value, strings_only=True)
if getattr(field, 'child', None): if getattr(field, "child", None):
field_info['child'] = self.get_field_info(field.child) field_info["child"] = self.get_field_info(field.child)
elif getattr(field, 'fields', None): elif getattr(field, "fields", None):
field_info['children'] = self.get_serializer_info(field) field_info["children"] = self.get_serializer_info(field)
is_related_field = isinstance(field, (serializers.RelatedField, serializers.ManyRelatedField))
if not is_related_field and hasattr(field, 'choices'):
field_info['choices'] = [
{
'value': choice_value,
'label': force_text(choice_name, strings_only=True)
}
for choice_value, choice_name in dict(field.choices).items()
]
class_name = field.__class__.__name__
if class_name == 'LabeledChoiceField':
field_info['type'] = 'labeled_choice'
elif class_name == 'ObjectRelatedField':
field_info['type'] = 'object_related_field'
elif class_name == 'ManyRelatedField':
child_relation_class_name = field.child_relation.__class__.__name__
if child_relation_class_name == 'ObjectRelatedField':
field_info['type'] = 'm2m_related_field'
# if field.label == '系统平台':
# print("Field: ", class_name, field, field_info)
if isinstance(field, TreeChoicesMixin):
self.set_tree_field(field, field_info)
elif isinstance(field, serializers.ChoiceField):
self.set_choices_field(field, field_info)
return field_info return field_info
def get_filters_fields(self, request, view): @staticmethod
def get_filters_fields(request, view):
fields = [] fields = []
if hasattr(view, 'get_filter_fields'): if hasattr(view, "get_filter_fields"):
fields = view.get_filter_fields(request) fields = view.get_filter_fields(request)
elif hasattr(view, 'filter_fields'): elif hasattr(view, "filter_fields"):
fields = view.filter_fields fields = view.filter_fields
elif hasattr(view, 'filterset_fields'): elif hasattr(view, "filterset_fields"):
fields = view.filterset_fields fields = view.filterset_fields
elif hasattr(view, 'get_filterset_fields'): elif hasattr(view, "get_filterset_fields"):
fields = view.get_filterset_fields(request) fields = view.get_filterset_fields(request)
elif hasattr(view, 'filterset_class'): elif hasattr(view, "filterset_class"):
fields = list(view.filterset_class.Meta.fields) + \ fields = list(view.filterset_class.Meta.fields) + list(
list(view.filterset_class.declared_filters.keys()) view.filterset_class.declared_filters.keys()
)
if hasattr(view, 'custom_filter_fields'): if hasattr(view, "custom_filter_fields"):
# 不能写 fields += view.custom_filter_fields # 不能写 fields += view.custom_filter_fields
# 会改变 view 的 filter_fields # 会改变 view 的 filter_fields
fields = list(fields) + list(view.custom_filter_fields) fields = list(fields) + list(view.custom_filter_fields)
@ -128,16 +149,19 @@ class SimpleMetadataWithFilters(SimpleMetadata):
fields = list(fields.keys()) fields = list(fields.keys())
return fields return fields
def get_ordering_fields(self, request, view): @staticmethod
def get_ordering_fields(request, view):
fields = [] fields = []
if hasattr(view, 'get_ordering_fields'): if hasattr(view, "get_ordering_fields"):
fields = view.get_ordering_fields(request) fields = view.get_ordering_fields(request)
elif hasattr(view, 'ordering_fields'): elif hasattr(view, "ordering_fields"):
fields = view.ordering_fields fields = view.ordering_fields
return fields return fields
def determine_metadata(self, request, view): def determine_metadata(self, request, view):
metadata = super(SimpleMetadataWithFilters, self).determine_metadata(request, view) metadata = super(SimpleMetadataWithFilters, self).determine_metadata(
request, view
)
filterset_fields = self.get_filters_fields(request, view) filterset_fields = self.get_filters_fields(request, view)
order_fields = self.get_ordering_fields(request, view) order_fields = self.get_ordering_fields(request, view)

View File

@ -9,14 +9,20 @@ from rest_framework.request import Request
from common.exceptions import UserConfirmRequired from common.exceptions import UserConfirmRequired
from audits.utils import create_operate_log from audits.utils import create_operate_log
from audits.models import OperateLog from audits.models import OperateLog
from audits.const import ActionChoices
__all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"] __all__ = [
"PermissionsMixin",
"RecordViewLogMixin",
"UserConfirmRequiredExceptionMixin",
]
class UserConfirmRequiredExceptionMixin: class UserConfirmRequiredExceptionMixin:
""" """
异常处理 异常处理
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -40,23 +46,23 @@ class PermissionsMixin(UserPassesTestMixin):
class RecordViewLogMixin: class RecordViewLogMixin:
ACTION = OperateLog.ACTION_VIEW ACTION = ActionChoices.view
@staticmethod @staticmethod
def get_resource_display(request): def get_resource_display(request):
query_params = dict(request.query_params) query_params = dict(request.query_params)
if query_params.get('format'): if query_params.get("format"):
query_params.pop('format') query_params.pop("format")
spm_filter = query_params.pop('spm') if query_params.get('spm') else None spm_filter = query_params.pop("spm") if query_params.get("spm") else None
if not query_params and not spm_filter: if not query_params and not spm_filter:
display_message = _('Export all') display_message = _("Export all")
elif spm_filter: elif spm_filter:
display_message = _('Export only selected items') display_message = _("Export only selected items")
else: else:
query = ','.join( query = ",".join(
['%s=%s' % (key, value) for key, value in query_params.items()] ["%s=%s" % (key, value) for key, value in query_params.items()]
) )
display_message = _('Export filtered: %s') % query display_message = _("Export filtered: %s") % query
return display_message return display_message
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):

View File

@ -1,5 +1,6 @@
import os import os
from django.utils.translation import ugettext_lazy as _
from django.core.mail import send_mail, EmailMultiAlternatives from django.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings from django.conf import settings
from celery import shared_task from celery import shared_task
@ -9,7 +10,7 @@ from .utils import get_logger
logger = get_logger(__file__) logger = get_logger(__file__)
@shared_task @shared_task(verbose_name=_("Send email"))
def send_mail_async(*args, **kwargs): def send_mail_async(*args, **kwargs):
""" Using celery to send email async """ Using celery to send email async
@ -36,7 +37,7 @@ def send_mail_async(*args, **kwargs):
logger.error("Sending mail error: {}".format(e)) logger.error("Sending mail error: {}".format(e))
@shared_task @shared_task(verbose_name=_("Send email attachment"))
def send_mail_attachment_async(subject, message, recipient_list, attachment_list=None): def send_mail_attachment_async(subject, message, recipient_list, attachment_list=None):
if attachment_list is None: if attachment_list is None:
attachment_list = [] attachment_list = []

View File

@ -344,7 +344,7 @@ def get_file_by_arch(dir, filename):
return file_path return file_path
def pretty_string(data: str, max_length=128, ellipsis_str='...'): def pretty_string(data, max_length=128, ellipsis_str='...'):
""" """
params: params:
data: abcdefgh data: abcdefgh
@ -353,6 +353,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
return: return:
ab...gh ab...gh
""" """
data = str(data)
if len(data) < max_length: if len(data) < max_length:
return data return data
remain_length = max_length - len(ellipsis_str) remain_length = max_length - len(ellipsis_str)

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638
size 73906864

View File

@ -0,0 +1,4 @@
def bit(x):
if x < 1:
raise ValueError("x must be greater than 1")
return 2 ** (x - 1)

View File

@ -7,23 +7,22 @@
2. 程序需要, 用户不需要更改的写到settings中 2. 程序需要, 用户不需要更改的写到settings中
3. 程序需要, 用户需要更改的写到本config中 3. 程序需要, 用户需要更改的写到本config中
""" """
import base64
import copy
import errno
import json
import logging
import os import os
import re import re
import sys import sys
import types import types
import errno
import json
import yaml
import copy
import base64
import logging
from importlib import import_module from importlib import import_module
from urllib.parse import urljoin, urlparse from urllib.parse import urljoin, urlparse
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
import yaml
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
@ -499,6 +498,9 @@ class Config(dict):
'FORGOT_PASSWORD_URL': '', 'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': '', 'HEALTH_CHECK_TOKEN': '',
# Applet 等软件的下载地址
'APPLET_DOWNLOAD_HOST': '',
} }
def __init__(self, *args): def __init__(self, *args):

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
default_interface = dict(( default_interface = dict((
('logo_logout', static('img/logo.png')), ('logo_logout', static('img/logo.png')),
('logo_index', static('img/logo_text.png')), ('logo_index', static('img/logo_text_white.png')),
('login_image', static('img/login_image.jpg')), ('login_image', static('img/login_image.jpg')),
('favicon', static('img/facio.ico')), ('favicon', static('img/facio.ico')),
('login_title', _('JumpServer Open Source Bastion Host')), ('login_title', _('JumpServer Open Source Bastion Host')),

View File

@ -1,4 +1,5 @@
import os import os
from django.urls import reverse_lazy from django.urls import reverse_lazy
from .. import const from .. import const
@ -36,6 +37,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV
# Absolute url for some case, for example email link # Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL SITE_URL = CONFIG.SITE_URL
# Absolute url for downloading applet
APPLET_DOWNLOAD_HOST = CONFIG.APPLET_DOWNLOAD_HOST
# https://docs.djangoproject.com/en/4.1/ref/settings/ # https://docs.djangoproject.com/en/4.1/ref/settings/
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@ -313,7 +317,6 @@ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
] ]
GMSSL_ENABLED = CONFIG.GMSSL_ENABLED GMSSL_ENABLED = CONFIG.GMSSL_ENABLED
GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher' GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher'
if GMSSL_ENABLED: if GMSSL_ENABLED:
@ -329,4 +332,3 @@ if os.environ.get('DEBUG_TOOLBAR', False):
DEBUG_TOOLBAR_PANELS = [ DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.profiling.ProfilingPanel', 'debug_toolbar.panels.profiling.ProfilingPanel',
] ]

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:07f1cfd07039142f4847b4139586bf815467f266119eae57476c073130f0ac92 oid sha256:adfa9c01178d5f6490e616f62d41c71974d42f9e3bd078fcf1b3c7124384df0b
size 118098 size 117024

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:314c29cb8b10aaddbb030bf49af293be23f0153ff1f1c7562946879574ce6de8 oid sha256:eeaa813f4ea052a1cd85b8ae5addfde6b088fd21a0261f8724d62823835512a2
size 102801 size 104043

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,6 @@ from .models import SystemMsgSubscription, UserMsgSubscription
__all__ = ('SystemMessage', 'UserMessage', 'system_msgs', 'Message') __all__ = ('SystemMessage', 'UserMessage', 'system_msgs', 'Message')
system_msgs = [] system_msgs = []
user_msgs = [] user_msgs = []
@ -44,7 +43,7 @@ class MessageType(type):
return clz return clz
@shared_task @shared_task(verbose_name=_('Publish the station message'))
def publish_task(msg): def publish_task(msg):
msg.publish() msg.publish()

View File

@ -5,6 +5,7 @@ class DefaultCallback:
STATUS_MAPPER = { STATUS_MAPPER = {
'successful': 'success', 'successful': 'success',
'failure': 'failed', 'failure': 'failed',
'failed': 'failed',
'running': 'running', 'running': 'running',
'pending': 'pending', 'pending': 'pending',
'unknown': 'unknown' 'unknown': 'unknown'

View File

@ -13,7 +13,7 @@ class AdHocRunner:
"reboot", 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' "reboot", 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top'
] ]
def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/'): def __init__(self, inventory, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars={}):
self.id = uuid.uuid4() self.id = uuid.uuid4()
self.inventory = inventory self.inventory = inventory
self.pattern = pattern self.pattern = pattern
@ -22,6 +22,7 @@ class AdHocRunner:
self.project_dir = project_dir self.project_dir = project_dir
self.cb = DefaultCallback() self.cb = DefaultCallback()
self.runner = None self.runner = None
self.extra_vars = extra_vars
def check_module(self): def check_module(self):
if self.module not in self.cmd_modules_choices: if self.module not in self.cmd_modules_choices:
@ -38,6 +39,7 @@ class AdHocRunner:
os.mkdir(self.project_dir, 0o755) os.mkdir(self.project_dir, 0o755)
ansible_runner.run( ansible_runner.run(
extravars=self.extra_vars,
host_pattern=self.pattern, host_pattern=self.pattern,
private_data_dir=self.project_dir, private_data_dir=self.project_dir,
inventory=self.inventory, inventory=self.inventory,

View File

@ -3,4 +3,4 @@ from django.conf import settings
def get_ansible_task_log_path(task_id): def get_ansible_task_log_path(task_id):
from ops.utils import get_task_log_path from ops.utils import get_task_log_path
return get_task_log_path(settings.ANSIBLE_LOG_DIR, task_id, level=3) return get_task_log_path(settings.CELERY_LOG_DIR, task_id, level=2)

View File

@ -2,3 +2,5 @@
# #
from .adhoc import * from .adhoc import *
from .celery import * from .celery import *
from .job import *
from .playbook import *

View File

@ -1,52 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.shortcuts import get_object_or_404 from rest_framework import viewsets
from rest_framework import viewsets, generics from ..models import AdHoc
from rest_framework.views import Response
from common.drf.serializers import CeleryTaskExecutionSerializer
from ..models import AdHoc, AdHocExecution
from ..serializers import ( from ..serializers import (
AdHocSerializer, AdHocSerializer
AdHocExecutionSerializer,
AdHocDetailSerializer,
) )
__all__ = [ __all__ = [
'AdHocViewSet', 'AdHocExecutionViewSet' 'AdHocViewSet'
] ]
class AdHocViewSet(viewsets.ModelViewSet): class AdHocViewSet(viewsets.ModelViewSet):
queryset = AdHoc.objects.all() queryset = AdHoc.objects.all()
serializer_class = AdHocSerializer serializer_class = AdHocSerializer
def get_serializer_class(self):
if self.action == 'retrieve':
return AdHocDetailSerializer
return super().get_serializer_class()
class AdHocExecutionViewSet(viewsets.ModelViewSet):
queryset = AdHocExecution.objects.all()
serializer_class = AdHocExecutionSerializer
def get_queryset(self):
task_id = self.request.query_params.get('task')
adhoc_id = self.request.query_params.get('adhoc')
if task_id:
task = get_object_or_404(AdHoc, id=task_id)
adhocs = task.adhoc.all()
self.queryset = self.queryset.filter(adhoc__in=adhocs)
if adhoc_id:
adhoc = get_object_or_404(AdHoc, id=adhoc_id)
self.queryset = self.queryset.filter(adhoc=adhoc)
return self.queryset

View File

@ -98,20 +98,27 @@ class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet):
return queryset return queryset
class CelerySummaryAPIView(generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
pass
class CeleryTaskViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet): class CeleryTaskViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet):
queryset = CeleryTask.objects.all()
serializer_class = CeleryTaskSerializer serializer_class = CeleryTaskSerializer
http_method_names = ('get', 'head', 'options',) http_method_names = ('get', 'head', 'options',)
def get_queryset(self):
return CeleryTask.objects.exclude(name__startswith='celery')
class CeleryTaskExecutionViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet): class CeleryTaskExecutionViewSet(CommonApiMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CeleryTaskExecutionSerializer serializer_class = CeleryTaskExecutionSerializer
http_method_names = ('get', 'head', 'options',) http_method_names = ('get', 'head', 'options',)
queryset = CeleryTaskExecution.objects.all()
def get_queryset(self): def get_queryset(self):
task_id = self.kwargs.get("task_pk") task_id = self.request.query_params.get('task_id')
if task_id: if task_id:
task = CeleryTask.objects.get(pk=task_id) task = get_object_or_404(CeleryTask, id=task_id)
return CeleryTaskExecution.objects.filter(name=task.name) self.queryset = self.queryset.filter(name=task.name)
else: return self.queryset
return CeleryTaskExecution.objects.none()

43
apps/ops/api/job.py Normal file
View File

@ -0,0 +1,43 @@
from rest_framework import viewsets
from ops.models import Job, JobExecution
from ops.serializers.job import JobSerializer, JobExecutionSerializer
__all__ = ['JobViewSet', 'JobExecutionViewSet']
from ops.tasks import run_ops_job, run_ops_job_executions
from orgs.mixins.api import OrgBulkModelViewSet
class JobViewSet(OrgBulkModelViewSet):
serializer_class = JobSerializer
model = Job
permission_classes = ()
def get_queryset(self):
query_set = super().get_queryset()
return query_set.filter(instant=False)
def perform_create(self, serializer):
instance = serializer.save()
if instance.instant:
run_ops_job.delay(instance.id)
class JobExecutionViewSet(OrgBulkModelViewSet):
serializer_class = JobExecutionSerializer
http_method_names = ('get', 'post', 'head', 'options',)
# filter_fields = ('type',)
permission_classes = ()
model = JobExecution
def perform_create(self, serializer):
instance = serializer.save()
run_ops_job_executions.delay(instance.id)
def get_queryset(self):
query_set = super().get_queryset()
job_id = self.request.query_params.get('job_id')
if job_id:
self.queryset = query_set.filter(job_id=job_id)
return query_set

28
apps/ops/api/playbook.py Normal file
View File

@ -0,0 +1,28 @@
import os
import zipfile
from django.conf import settings
from rest_framework import viewsets
from ..models import Playbook
from ..serializers.playbook import PlaybookSerializer
__all__ = ["PlaybookViewSet"]
def unzip_playbook(src, dist):
fz = zipfile.ZipFile(src, 'r')
for file in fz.namelist():
fz.extract(file, dist)
class PlaybookViewSet(viewsets.ModelViewSet):
queryset = Playbook.objects.all()
serializer_class = PlaybookSerializer
def perform_create(self, serializer):
instance = serializer.save()
src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name)
dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
if os.path.exists(dest_path):
os.makedirs(dest_path)
unzip_playbook(src_path, dest_path)

View File

@ -0,0 +1,171 @@
# Generated by Django 3.2.14 on 2022-11-11 11:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0111_alter_automationexecution_status'),
('ops', '0028_celerytask_last_published_time'),
]
operations = [
migrations.CreateModel(
name='Job',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, null=True, verbose_name='Name')),
('instant', models.BooleanField(default=False)),
('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')),
('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', max_length=128, null=True, verbose_name='Module')),
('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', max_length=128, verbose_name='Type')),
('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')),
('runas_policy', models.CharField(choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')),
('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')),
('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='JobExecution',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('task_id', models.UUIDField(null=True)),
('status', models.CharField(default='running', max_length=16, verbose_name='Status')),
('result', models.JSONField(blank=True, null=True, verbose_name='Result')),
('summary', models.JSONField(default=dict, verbose_name='Summary')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')),
('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')),
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
('job', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='ops.job')),
],
options={
'abstract': False,
},
),
migrations.AlterUniqueTogether(
name='playbooktemplate',
unique_together=None,
),
migrations.RemoveField(
model_name='adhoc',
name='account',
),
migrations.RemoveField(
model_name='adhoc',
name='account_policy',
),
migrations.RemoveField(
model_name='adhoc',
name='assets',
),
migrations.RemoveField(
model_name='adhoc',
name='crontab',
),
migrations.RemoveField(
model_name='adhoc',
name='date_last_run',
),
migrations.RemoveField(
model_name='adhoc',
name='interval',
),
migrations.RemoveField(
model_name='adhoc',
name='is_periodic',
),
migrations.RemoveField(
model_name='adhoc',
name='last_execution',
),
migrations.RemoveField(
model_name='adhoc',
name='org_id',
),
migrations.RemoveField(
model_name='playbook',
name='account',
),
migrations.RemoveField(
model_name='playbook',
name='account_policy',
),
migrations.RemoveField(
model_name='playbook',
name='assets',
),
migrations.RemoveField(
model_name='playbook',
name='comment',
),
migrations.RemoveField(
model_name='playbook',
name='crontab',
),
migrations.RemoveField(
model_name='playbook',
name='date_last_run',
),
migrations.RemoveField(
model_name='playbook',
name='interval',
),
migrations.RemoveField(
model_name='playbook',
name='is_periodic',
),
migrations.RemoveField(
model_name='playbook',
name='last_execution',
),
migrations.RemoveField(
model_name='playbook',
name='org_id',
),
migrations.RemoveField(
model_name='playbook',
name='template',
),
migrations.AlterField(
model_name='adhoc',
name='module',
field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', max_length=128, verbose_name='Module'),
),
migrations.AlterField(
model_name='playbook',
name='name',
field=models.CharField(max_length=128, null=True, verbose_name='Name'),
),
migrations.AlterField(
model_name='playbook',
name='path',
field=models.FileField(upload_to='playbooks/'),
),
migrations.DeleteModel(
name='PlaybookExecution',
),
migrations.DeleteModel(
name='PlaybookTemplate',
),
migrations.AddField(
model_name='job',
name='playbook',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbook', verbose_name='Playbook'),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.2.14 on 2022-11-16 10:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0029_auto_20221111_1919'),
]
operations = [
migrations.AlterModelOptions(
name='celerytask',
options={'ordering': ('name',)},
),
migrations.AddField(
model_name='job',
name='variables',
field=models.JSONField(default=dict, verbose_name='Variables'),
),
migrations.AlterField(
model_name='celerytask',
name='name',
field=models.CharField(max_length=1024, verbose_name='Name'),
),
migrations.AlterField(
model_name='celerytaskexecution',
name='date_finished',
field=models.DateTimeField(null=True, verbose_name='Date finished'),
),
migrations.AlterField(
model_name='celerytaskexecution',
name='date_published',
field=models.DateTimeField(auto_now_add=True, verbose_name='Date published'),
),
migrations.AlterField(
model_name='celerytaskexecution',
name='date_start',
field=models.DateTimeField(null=True, verbose_name='Date start'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.14 on 2022-11-16 12:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0030_auto_20221116_1811'),
]
operations = [
migrations.AddField(
model_name='job',
name='chdir',
field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir'),
),
migrations.AddField(
model_name='job',
name='comment',
field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'),
),
migrations.AddField(
model_name='job',
name='timeout',
field=models.IntegerField(default=60, verbose_name='Timeout (Seconds)'),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.14 on 2022-11-17 10:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0031_auto_20221116_2024'),
]
operations = [
migrations.RemoveField(
model_name='job',
name='variables',
),
migrations.AddField(
model_name='job',
name='parameters_define',
field=models.JSONField(default=dict, verbose_name='Parameters define'),
),
migrations.AddField(
model_name='jobexecution',
name='parameters',
field=models.JSONField(default=dict, verbose_name='Parameters'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.14 on 2022-11-18 06:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0032_auto_20221117_1848'),
]
operations = [
migrations.AddField(
model_name='job',
name='crontab',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform'),
),
migrations.AddField(
model_name='job',
name='interval',
field=models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform'),
),
migrations.AddField(
model_name='job',
name='is_periodic',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-23 09:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0033_auto_20221118_1431'),
]
operations = [
migrations.AddField(
model_name='job',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-11-23 10:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0034_job_org_id'),
]
operations = [
migrations.AddField(
model_name='jobexecution',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@ -4,3 +4,4 @@
from .adhoc import * from .adhoc import *
from .celery import * from .celery import *
from .playbook import * from .playbook import *
from .job import *

View File

@ -1,29 +1,43 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import os.path import os.path
import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.db.models import BaseCreateUpdateModel
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAnsibleJob, BaseAnsibleExecution from .base import BaseAnsibleJob, BaseAnsibleExecution
from ..ansible import AdHocRunner from ..ansible import AdHocRunner
__all__ = ["AdHoc", "AdHocExecution"] __all__ = ["AdHoc", "AdHocExecution"]
logger = get_logger(__file__) logger = get_logger(__file__)
class AdHoc(BaseAnsibleJob): class AdHoc(BaseCreateUpdateModel):
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all') class Modules(models.TextChoices):
module = models.CharField(max_length=128, default='shell', verbose_name=_('Module')) shell = 'shell', _('Shell')
args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) winshell = 'win_shell', _('Powershell')
last_execution = models.ForeignKey('AdHocExecution', verbose_name=_("Last execution"),
on_delete=models.SET_NULL, null=True, blank=True)
def get_register_task(self): id = models.UUIDField(default=uuid.uuid4, primary_key=True)
from ops.tasks import run_adhoc name = models.CharField(max_length=128, verbose_name=_('Name'))
return "run_adhoc_{}".format(self.id), run_adhoc, (str(self.id),), {} pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
verbose_name=_('Module'))
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))
owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
@property
def row_count(self):
if len(self.args) == 0:
return 0
count = str(self.args).count('\n')
return count + 1
@property
def size(self):
return len(self.args)
def __str__(self): def __str__(self):
return "{}: {}".format(self.module, self.args) return "{}: {}".format(self.module, self.args)

View File

@ -17,7 +17,8 @@ class BaseAnsibleJob(PeriodTaskModelMixin, JMSOrgBaseModel):
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
account = models.CharField(max_length=128, default='root', verbose_name=_('Account')) account = models.CharField(max_length=128, default='root', verbose_name=_('Account'))
account_policy = models.CharField(max_length=128, default='root', verbose_name=_('Account policy')) account_policy = models.CharField(max_length=128, default='root', verbose_name=_('Account policy'))
last_execution = models.ForeignKey('BaseAnsibleExecution', verbose_name=_("Last execution"), on_delete=models.SET_NULL, null=True) last_execution = models.ForeignKey('BaseAnsibleExecution', verbose_name=_("Last execution"),
on_delete=models.SET_NULL, null=True)
date_last_run = models.DateTimeField(null=True, verbose_name=_('Date last run')) date_last_run = models.DateTimeField(null=True, verbose_name=_('Date last run'))
class Meta: class Meta:
@ -118,12 +119,6 @@ class BaseAnsibleExecution(models.Model):
def is_success(self): def is_success(self):
return self.status == 'success' return self.status == 'success'
@property
def time_cost(self):
if self.date_finished and self.date_start:
return (self.date_finished - self.date_start).total_seconds()
return None
@property @property
def short_id(self): def short_id(self):
return str(self.id).split('-')[-1] return str(self.id).split('-')[-1]
@ -134,4 +129,8 @@ class BaseAnsibleExecution(models.Model):
return self.date_finished - self.date_start return self.date_finished - self.date_start
return None return None
@property
def time_cost(self):
if self.date_finished and self.date_start:
return (self.date_finished - self.date_start).total_seconds()
return None

View File

@ -12,18 +12,24 @@ from ops.celery import app
class CeleryTask(models.Model): class CeleryTask(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4) id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=1024) name = models.CharField(max_length=1024, verbose_name=_('Name'))
last_published_time = models.DateTimeField(null=True) last_published_time = models.DateTimeField(null=True)
@property @property
def meta(self): def meta(self):
task = app.tasks.get(self.name, None) task = app.tasks.get(self.name, None)
return { return {
"verbose_name": getattr(task, 'verbose_name', None), "comment": getattr(task, 'verbose_name', None),
"comment": getattr(task, 'comment', None),
"queue": getattr(task, 'queue', 'default') "queue": getattr(task, 'queue', 'default')
} }
@property
def summary(self):
executions = CeleryTaskExecution.objects.filter(name=self.name)
total = executions.count()
success = executions.filter(state='SUCCESS').count()
return {'total': total, 'success': success}
@property @property
def state(self): def state(self):
last_five_executions = CeleryTaskExecution.objects.filter(name=self.name).order_by('-date_published')[:5] last_five_executions = CeleryTaskExecution.objects.filter(name=self.name).order_by('-date_published')[:5]
@ -37,6 +43,9 @@ class CeleryTask(models.Model):
return "yellow" return "yellow"
return "green" return "green"
class Meta:
ordering = ('name',)
class CeleryTaskExecution(models.Model): class CeleryTaskExecution(models.Model):
LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery') LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery')
@ -46,9 +55,21 @@ class CeleryTaskExecution(models.Model):
kwargs = models.JSONField(verbose_name=_("Kwargs")) kwargs = models.JSONField(verbose_name=_("Kwargs"))
state = models.CharField(max_length=16, verbose_name=_("State")) state = models.CharField(max_length=16, verbose_name=_("State"))
is_finished = models.BooleanField(default=False, verbose_name=_("Finished")) is_finished = models.BooleanField(default=False, verbose_name=_("Finished"))
date_published = models.DateTimeField(auto_now_add=True) date_published = models.DateTimeField(auto_now_add=True, verbose_name=_('Date published'))
date_start = models.DateTimeField(null=True) date_start = models.DateTimeField(null=True, verbose_name=_('Date start'))
date_finished = models.DateTimeField(null=True) date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished'))
@property
def time_cost(self):
if self.date_finished and self.date_start:
return (self.date_finished - self.date_start).total_seconds()
return None
@property
def timedelta(self):
if self.date_start and self.date_finished:
return self.date_finished - self.date_start
return None
def __str__(self): def __str__(self):
return "{}: {}".format(self.name, self.id) return "{}: {}".format(self.name, self.id)

View File

@ -0,0 +1,4 @@
# 内置环境变量
BUILTIN_VARIABLES = {
}

200
apps/ops/models/job.py Normal file
View File

@ -0,0 +1,200 @@
import json
import os
import uuid
import logging
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from celery import current_task
__all__ = ["Job", "JobExecution"]
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import JMSOrgBaseModel
class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
class Types(models.TextChoices):
adhoc = 'adhoc', _('Adhoc')
playbook = 'playbook', _('Playbook')
class RunasPolicies(models.TextChoices):
privileged_only = 'privileged_only', _('Privileged Only')
privileged_first = 'privileged_first', _('Privileged First')
skip = 'skip', _('Skip')
class Modules(models.TextChoices):
shell = 'shell', _('Shell')
winshell = 'win_shell', _('Powershell')
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, null=True, verbose_name=_('Name'))
instant = models.BooleanField(default=False)
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'), null=True, blank=True)
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
verbose_name=_('Module'), null=True)
chdir = models.CharField(default="", max_length=1024, verbose_name=_('Chdir'), null=True, blank=True)
timeout = models.IntegerField(default=60, verbose_name=_('Timeout (Seconds)'))
playbook = models.ForeignKey('ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.SET_NULL)
type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc, verbose_name=_("Type"))
owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas'))
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
verbose_name=_('Runas policy'))
parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define'))
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
@property
def last_execution(self):
return self.executions.last()
@property
def date_last_run(self):
return self.last_execution.date_created if self.last_execution else None
@property
def summary(self):
summary = {
"total": 0,
"success": 0,
}
for execution in self.executions.all():
summary["total"] += 1
if execution.is_success:
summary["success"] += 1
return summary
@property
def average_time_cost(self):
total_cost = 0
finished_count = self.executions.filter(status__in=['success', 'failed']).count()
for execution in self.executions.filter(status__in=['success', 'failed']).all():
total_cost += execution.time_cost
return total_cost / finished_count if finished_count else 0
def get_register_task(self):
from ..tasks import run_ops_job
name = "run_ops_job_period_{}".format(str(self.id)[:8])
task = run_ops_job.name
args = (str(self.id),)
kwargs = {}
return name, task, args, kwargs
@property
def inventory(self):
return JMSInventory(self.assets.all(), self.runas_policy, self.runas)
def create_execution(self):
return self.executions.create()
class JobExecution(JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
task_id = models.UUIDField(null=True)
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='executions', null=True)
parameters = models.JSONField(default=dict, verbose_name=_('Parameters'))
result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
def get_runner(self):
inv = self.job.inventory
inv.write_to_file(self.inventory_path)
if isinstance(self.parameters, str):
extra_vars = json.loads(self.parameters)
else:
extra_vars = {}
if self.job.type == 'adhoc':
runner = AdHocRunner(
self.inventory_path, self.job.module, module_args=self.job.args,
pattern="all", project_dir=self.private_dir, extra_vars=extra_vars,
)
elif self.job.type == 'playbook':
runner = PlaybookRunner(
self.inventory_path, self.job.playbook.work_path
)
else:
raise Exception("unsupported job type")
return runner
@property
def short_id(self):
return str(self.id).split('-')[-1]
@property
def time_cost(self):
if self.date_finished and self.date_start:
return (self.date_finished - self.date_start).total_seconds()
return None
@property
def timedelta(self):
if self.date_start and self.date_finished:
return self.date_finished - self.date_start
return None
@property
def is_finished(self):
return self.status in ['success', 'failed']
@property
def is_success(self):
return self.status == 'success'
@property
def inventory_path(self):
return os.path.join(self.private_dir, 'inventory', 'hosts')
@property
def private_dir(self):
uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id
job_name = self.job.name if self.job.name else 'instant'
return os.path.join(settings.ANSIBLE_DIR, job_name, uniq)
def set_error(self, error):
this = self.__class__.objects.get(id=self.id) # 重新获取一次,避免数据库超时连接超时
this.status = 'failed'
this.summary['error'] = str(error)
this.finish_task()
def set_result(self, cb):
status_mapper = {
'successful': 'success',
}
this = self.__class__.objects.get(id=self.id)
this.status = status_mapper.get(cb.status, cb.status)
this.summary = cb.summary
this.result = cb.result
this.finish_task()
def finish_task(self):
self.date_finished = timezone.now()
self.save(update_fields=['result', 'status', 'summary', 'date_finished'])
def set_celery_id(self):
if not current_task:
return
task_id = current_task.request.root_id
self.task_id = task_id
def start(self, **kwargs):
self.date_start = timezone.now()
self.set_celery_id()
self.save()
runner = self.get_runner()
try:
cb = runner.run(**kwargs)
self.set_result(cb)
return cb
except Exception as e:
logging.error(e, exc_info=True)
self.set_error(e)

View File

@ -1,39 +1,19 @@
import os.path
import uuid
from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from orgs.mixins.models import JMSOrgBaseModel from common.db.models import BaseCreateUpdateModel
from .base import BaseAnsibleExecution, BaseAnsibleJob
class PlaybookTemplate(JMSOrgBaseModel): class Playbook(BaseCreateUpdateModel):
name = models.CharField(max_length=128, verbose_name=_("Name")) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
path = models.FilePathField(verbose_name=_("Path")) name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
comment = models.TextField(verbose_name=_("Comment"), blank=True) path = models.FileField(upload_to='playbooks/')
def __str__(self):
return self.name
class Meta:
ordering = ['name']
verbose_name = _("Playbook template")
unique_together = [('org_id', 'name')]
class Playbook(BaseAnsibleJob):
path = models.FilePathField(max_length=1024, verbose_name=_("Playbook"))
owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True) owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True)
comment = models.TextField(blank=True, verbose_name=_("Comment"))
template = models.ForeignKey('PlaybookTemplate', verbose_name=_("Template"), on_delete=models.SET_NULL, null=True)
last_execution = models.ForeignKey('PlaybookExecution', verbose_name=_("Last execution"), on_delete=models.SET_NULL, null=True, blank=True)
def get_register_task(self): @property
name = "automation_strategy_period_{}".format(str(self.id)[:8]) def work_path(self):
task = execute_automation_strategy.name return os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__(), "main.yaml")
args = (str(self.id), Trigger.timing)
kwargs = {}
return name, task, args, kwargs
class PlaybookExecution(BaseAnsibleExecution):
task = models.ForeignKey('Playbook', verbose_name=_("Task"), on_delete=models.CASCADE)
path = models.FilePathField(max_length=1024, verbose_name=_("Run dir"))

View File

@ -1,11 +1,24 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import serializers
from django.shortcuts import reverse
import datetime
from rest_framework import serializers
from common.drf.fields import ReadableHiddenField
from ..models import AdHoc, AdHocExecution from ..models import AdHoc, AdHocExecution
class AdHocSerializer(serializers.ModelSerializer):
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
row_count = serializers.IntegerField(read_only=True)
size = serializers.IntegerField(read_only=True)
class Meta:
model = AdHoc
fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"]
class AdHocExecutionSerializer(serializers.ModelSerializer): class AdHocExecutionSerializer(serializers.ModelSerializer):
stat = serializers.SerializerMethodField() stat = serializers.SerializerMethodField()
last_success = serializers.ListField(source='success_hosts') last_success = serializers.ListField(source='success_hosts')
@ -49,26 +62,6 @@ class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer):
] ]
class AdHocSerializer(serializers.ModelSerializer):
tasks = serializers.ListField()
class Meta:
model = AdHoc
fields_mini = ['id']
fields_small = fields_mini + [
'tasks', "pattern", "args", "date_created",
]
fields_fk = ["last_execution"]
fields_m2m = ["assets"]
fields = fields_small + fields_fk + fields_m2m
read_only_fields = [
'date_created'
]
extra_kwargs = {
"become": {'write_only': True}
}
class AdHocExecutionNestSerializer(serializers.ModelSerializer): class AdHocExecutionNestSerializer(serializers.ModelSerializer):
last_success = serializers.ListField(source='success_hosts') last_success = serializers.ListField(source='success_hosts')
last_failure = serializers.DictField(source='failed_hosts') last_failure = serializers.DictField(source='failed_hosts')
@ -80,38 +73,3 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer):
'last_success', 'last_failure', 'last_run', 'timedelta', 'last_success', 'last_failure', 'last_run', 'timedelta',
'is_finished', 'is_success' 'is_finished', 'is_success'
) )
class AdHocDetailSerializer(AdHocSerializer):
latest_execution = AdHocExecutionNestSerializer(allow_null=True)
task_name = serializers.CharField(source='task.name')
class Meta(AdHocSerializer.Meta):
fields = AdHocSerializer.Meta.fields + [
'latest_execution', 'created_by', 'task_name'
]
# class CommandExecutionSerializer(serializers.ModelSerializer):
# result = serializers.JSONField(read_only=True)
# log_url = serializers.SerializerMethodField()
#
# class Meta:
# model = CommandExecution
# fields_mini = ['id']
# fields_small = fields_mini + [
# 'command', 'result', 'log_url',
# 'is_finished', 'date_created', 'date_finished'
# ]
# fields_m2m = ['hosts']
# fields = fields_small + fields_m2m
# read_only_fields = [
# 'result', 'is_finished', 'log_url', 'date_created',
# 'date_finished'
# ]
# ref_name = 'OpsCommandExecution'
#
# @staticmethod
# def get_log_url(obj):
# return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})

View File

@ -30,14 +30,15 @@ class CeleryPeriodTaskSerializer(serializers.ModelSerializer):
class CeleryTaskSerializer(serializers.ModelSerializer): class CeleryTaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CeleryTask model = CeleryTask
fields = [ read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'last_published_time']
'id', 'name', 'meta', 'state', 'last_published_time', fields = read_only_fields
]
class CeleryTaskExecutionSerializer(serializers.ModelSerializer): class CeleryTaskExecutionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CeleryTaskExecution model = CeleryTaskExecution
fields = [ fields = [
"id", "name", "args", "kwargs", "state", "is_finished", "date_published", "date_start", "date_finished" "id", "name", "args", "kwargs", "time_cost", "timedelta", "state", "is_finished", "date_published",
"date_start",
"date_finished"
] ]

View File

@ -0,0 +1,34 @@
from rest_framework import serializers
from common.drf.fields import ReadableHiddenField
from ops.mixin import PeriodTaskSerializerMixin
from ops.models import Job, JobExecution
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
_all_ = []
class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Job
read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"]
fields = read_only_fields + [
"name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner",
"parameters_define",
"timeout",
"chdir",
"comment",
"summary",
"is_periodic", "interval", "crontab"
]
class JobExecutionSerializer(serializers.ModelSerializer):
class Meta:
model = JobExecution
read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created',
'is_success', 'task_id', 'short_id']
fields = read_only_fields + [
"job", "parameters"
]

View File

@ -0,0 +1,28 @@
import os
from rest_framework import serializers
from common.drf.fields import ReadableHiddenField
from ops.models import Playbook
def parse_playbook_name(path):
file_name = os.path.split(path)[-1]
return file_name.split(".")[-2]
class PlaybookSerializer(serializers.ModelSerializer):
owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
def create(self, validated_data):
name = validated_data.get('name')
if not name:
path = validated_data.get('path').name
validated_data['name'] = parse_playbook_name(path)
return super().create(validated_data)
class Meta:
model = Playbook
fields = [
"id", "name", "path", "date_created", "owner", "date_updated"
]

View File

@ -1,14 +1,16 @@
import ast import ast
from celery import signals
from django.db import transaction from django.db import transaction
from django.core.cache import cache
from django.dispatch import receiver from django.dispatch import receiver
from django.db.utils import ProgrammingError
from django.utils import translation, timezone from django.utils import translation, timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.core.cache import cache
from celery import signals, current_app
from common.db.utils import close_old_connections, get_logger
from common.signals import django_ready from common.signals import django_ready
from common.db.utils import close_old_connections, get_logger
from .celery import app from .celery import app
from .models import CeleryTaskExecution, CeleryTask from .models import CeleryTaskExecution, CeleryTask
@ -23,8 +25,6 @@ def sync_registered_tasks(*args, **kwargs):
with transaction.atomic(): with transaction.atomic():
try: try:
db_tasks = CeleryTask.objects.all() db_tasks = CeleryTask.objects.all()
except Exception as e:
return
celery_task_names = [key for key in app.tasks] celery_task_names = [key for key in app.tasks]
db_task_names = db_tasks.values_list('name', flat=True) db_task_names = db_tasks.values_list('name', flat=True)
@ -32,6 +32,8 @@ def sync_registered_tasks(*args, **kwargs):
not_in_db_tasks = set(celery_task_names) - set(db_task_names) not_in_db_tasks = set(celery_task_names) - set(db_task_names)
tasks_to_create = [CeleryTask(name=name) for name in not_in_db_tasks] tasks_to_create = [CeleryTask(name=name) for name in not_in_db_tasks]
CeleryTask.objects.bulk_create(tasks_to_create) CeleryTask.objects.bulk_create(tasks_to_create)
except ProgrammingError:
pass
@signals.before_task_publish.connect @signals.before_task_publish.connect

View File

@ -1,18 +1,16 @@
# coding: utf-8 # coding: utf-8
import os import os
import random
import subprocess import subprocess
from django.conf import settings from django.conf import settings
from celery import shared_task, subtask from celery import shared_task
from celery import signals
from celery.exceptions import SoftTimeLimitExceeded from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, gettext from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, get_object_or_none, get_log_keep_day from common.utils import get_logger, get_object_or_none, get_log_keep_day
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_org
from .celery.decorator import ( from .celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic, register_as_period_task, after_app_shutdown_clean_periodic,
after_app_ready_start after_app_ready_start
@ -21,32 +19,19 @@ from .celery.utils import (
create_or_update_celery_periodic_tasks, get_celery_periodic_task, create_or_update_celery_periodic_tasks, get_celery_periodic_task,
disable_celery_periodic_task, delete_celery_periodic_task disable_celery_periodic_task, delete_celery_periodic_task
) )
from .models import CeleryTaskExecution, AdHoc, Playbook from .models import CeleryTaskExecution, Job, JobExecution
from .notifications import ServerPerformanceCheckUtil from .notifications import ServerPerformanceCheckUtil
logger = get_logger(__file__) logger = get_logger(__file__)
def rerun_task():
pass
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task")) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task"))
def run_adhoc(tid, **kwargs): def run_ops_job(job_id):
""" job = get_object_or_none(Job, id=job_id)
:param tid: is the tasks serialized data with tmp_to_org(job.org):
:param callback: callback function name execution = job.create_execution()
:return:
"""
with tmp_to_root_org():
task = get_object_or_none(AdHoc, id=tid)
if not task:
logger.error("No task found")
return
with tmp_to_org(task.org):
execution = task.create_execution()
try: try:
execution.start(**kwargs) execution.start()
except SoftTimeLimitExceeded: except SoftTimeLimitExceeded:
execution.set_error('Run timeout') execution.set_error('Run timeout')
logger.error("Run adhoc timeout") logger.error("Run adhoc timeout")
@ -55,40 +40,21 @@ def run_adhoc(tid, **kwargs):
logger.error("Start adhoc execution error: {}".format(e)) logger.error("Start adhoc execution error: {}".format(e))
@shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible command")) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution"))
def run_playbook(pid, **kwargs): def run_ops_job_executions(execution_id, **kwargs):
with tmp_to_root_org(): execution = get_object_or_none(JobExecution, id=execution_id)
task = get_object_or_none(Playbook, id=pid) with tmp_to_org(execution.org):
if not task:
logger.error("No task found")
return
with tmp_to_org(task.org):
execution = task.create_execution()
try: try:
execution.start(**kwargs) execution.start()
except SoftTimeLimitExceeded: except SoftTimeLimitExceeded:
execution.set_error('Run timeout') execution.set_error('Run timeout')
logger.error("Run playbook timeout") logger.error("Run adhoc timeout")
except Exception as e: except Exception as e:
execution.set_error(e) execution.set_error(e)
logger.error("Run playbook execution error: {}".format(e)) logger.error("Start adhoc execution error: {}".format(e))
@shared_task @shared_task(verbose_name=_('Periodic clear celery tasks'))
@after_app_shutdown_clean_periodic
@register_as_period_task(interval=3600 * 24, description=_("Clean task history period"))
def clean_tasks_adhoc_period():
logger.debug("Start clean task adhoc and run history")
tasks = Task.objects.all()
for task in tasks:
adhoc = task.adhoc.all().order_by('-date_created')[5:]
for ad in adhoc:
ad.execution.all().delete()
ad.delete()
@shared_task
@after_app_shutdown_clean_periodic @after_app_shutdown_clean_periodic
@register_as_period_task(interval=3600 * 24, description=_("Clean celery log period")) @register_as_period_task(interval=3600 * 24, description=_("Clean celery log period"))
def clean_celery_tasks_period(): def clean_celery_tasks_period():
@ -107,7 +73,7 @@ def clean_celery_tasks_period():
subprocess.call(command, shell=True) subprocess.call(command, shell=True)
@shared_task @shared_task(verbose_name=_('Clear celery periodic tasks'))
@after_app_ready_start @after_app_ready_start
def clean_celery_periodic_tasks(): def clean_celery_periodic_tasks():
"""清除celery定时任务""" """清除celery定时任务"""
@ -130,7 +96,7 @@ def clean_celery_periodic_tasks():
logger.info('Clean task failure: {}'.format(task)) logger.info('Clean task failure: {}'.format(task))
@shared_task @shared_task(verbose_name=_('Create or update periodic tasks'))
@after_app_ready_start @after_app_ready_start
def create_or_update_registered_periodic_tasks(): def create_or_update_registered_periodic_tasks():
from .celery.decorator import get_register_period_tasks from .celery.decorator import get_register_period_tasks
@ -138,37 +104,7 @@ def create_or_update_registered_periodic_tasks():
create_or_update_celery_periodic_tasks(task) create_or_update_celery_periodic_tasks(task)
@shared_task @shared_task(verbose_name=_("Periodic check service performance"))
@register_as_period_task(interval=3600) @register_as_period_task(interval=3600)
def check_server_performance_period(): def check_server_performance_period():
ServerPerformanceCheckUtil().check_and_publish() ServerPerformanceCheckUtil().check_and_publish()
@shared_task(verbose_name=_("Hello"), comment="an test shared task")
def hello(name, callback=None):
from users.models import User
import time
count = User.objects.count()
print(gettext("Hello") + ': ' + name)
print("Count: ", count)
time.sleep(1)
return gettext("Hello")
@shared_task(verbose_name="Hello Error", comment="an test shared task error")
def hello_error():
raise Exception("must be error")
@shared_task(verbose_name="Hello Random", comment="some time error and some time success")
def hello_random():
i = random.randint(0, 1)
if i == 1:
raise Exception("must be error")
@shared_task
def hello_callback(result):
print(result)
print("Hello callback")

View File

@ -4,7 +4,6 @@ from __future__ import unicode_literals
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from rest_framework_nested import routers
from .. import api from .. import api
@ -13,23 +12,25 @@ app_name = "ops"
router = DefaultRouter() router = DefaultRouter()
bulk_router = BulkRouter() bulk_router = BulkRouter()
router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'adhocs', api.AdHocViewSet, 'adhoc')
router.register(r'adhoc-executions', api.AdHocExecutionViewSet, 'execution') router.register(r'playbooks', api.PlaybookViewSet, 'playbook')
router.register(r'jobs', api.JobViewSet, 'job')
router.register(r'job-executions', api.JobExecutionViewSet, 'job-execution')
router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task')
router.register(r'tasks', api.CeleryTaskViewSet, 'task') router.register(r'tasks', api.CeleryTaskViewSet, 'task')
router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-executions')
task_router = routers.NestedDefaultRouter(router, r'tasks', lookup='task')
task_router.register(r'executions', api.CeleryTaskExecutionViewSet, 'task-execution')
urlpatterns = [ urlpatterns = [
path('ansible/job-execution/<uuid:pk>/log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'),
path('celery/task/<uuid:name>/task-execution/<uuid:pk>/log/', api.CeleryTaskExecutionLogApi.as_view(), path('celery/task/<uuid:name>/task-execution/<uuid:pk>/log/', api.CeleryTaskExecutionLogApi.as_view(),
name='celery-task-execution-log'), name='celery-task-execution-log'),
path('celery/task/<uuid:name>/task-execution/<uuid:pk>/result/', api.CeleryResultApi.as_view(), path('celery/task/<uuid:name>/task-execution/<uuid:pk>/result/', api.CeleryResultApi.as_view(),
name='celery-task-execution-result'), name='celery-task-execution-result'),
path('ansible/task-execution/<uuid:pk>/log/', api.AnsibleTaskLogApi.as_view(), name='ansible-task-log'),
] ]
urlpatterns += (router.urls + bulk_router.urls + task_router.urls) urlpatterns += (router.urls + bulk_router.urls)

Some files were not shown because too many files have changed in this diff Show More