perf: merge with dev

pull/8023/head
ibuler 2022-07-17 14:28:55 +08:00
commit 93a89509d6
413 changed files with 11109 additions and 10465 deletions

1
.gitattributes vendored
View File

@ -1,2 +1,3 @@
*.mmdb filter=lfs diff=lfs merge=lfs -text *.mmdb filter=lfs diff=lfs merge=lfs -text
*.mo filter=lfs diff=lfs merge=lfs -text *.mo filter=lfs diff=lfs merge=lfs -text
*.ipdb filter=lfs diff=lfs merge=lfs -text

3
.gitignore vendored
View File

@ -34,10 +34,11 @@ celerybeat-schedule.db
data/static data/static
docs/_build/ docs/_build/
xpack xpack
xpack.bak
logs/* logs/*
### Vagrant ### ### Vagrant ###
.vagrant/ .vagrant/
release/* release/*
releashe releashe
/apps/script.py /apps/script.py
xpack.bak data/*

View File

@ -1,20 +1,5 @@
# 编译代码
FROM python:3.8-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim FROM python:3.8-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple MAINTAINER JumpServer Team <ibuler@qq.com>
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
WORKDIR /opt/jumpserver
ARG BUILD_DEPENDENCIES=" \ ARG BUILD_DEPENDENCIES=" \
g++ \ g++ \
@ -44,11 +29,12 @@ ARG TOOLS=" \
redis-tools \ redis-tools \
telnet \ telnet \
vim \ vim \
unzip \
wget" wget"
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ && sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \ && apt update && sleep 1 && apt update \
&& apt -y install ${BUILD_DEPENDENCIES} \ && apt -y install ${BUILD_DEPENDENCIES} \
&& apt -y install ${DEPENDENCIES} \ && apt -y install ${DEPENDENCIES} \
&& apt -y install ${TOOLS} \ && apt -y install ${TOOLS} \
@ -62,21 +48,44 @@ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& mv /bin/sh /bin/sh.bak \ && mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh && ln -s /bin/bash /bin/sh
RUN mkdir -p /opt/jumpserver/oracle/ \ ARG TARGETARCH
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \ ARG ORACLE_LIB_MAJOR=19
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \ ARG ORACLE_LIB_MINOR=10
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \ ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
RUN mkdir -p /opt/oracle/ \
&& cd /opt/oracle/ \
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \ && ldconfig \
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar && rm -f ${ORACLE_FILE}
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver WORKDIR /tmp/build
COPY ./requirements ./requirements
RUN echo > config.yml \ ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
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 $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \ && pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip && rm -rf ~/.cache/pip
ARG VERSION
ENV VERSION=$VERSION
ADD . .
RUN cd utils \
&& bash -ixeu build.sh \
&& mv ../release/jumpserver /opt/jumpserver \
&& rm -rf /tmp/build \
&& echo > /opt/jumpserver/config.yml
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs VOLUME /opt/jumpserver/logs

View File

@ -1,10 +1,13 @@
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p> <p align="center">
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
</p>
<h3 align="center">多云环境下更好用的堡垒机</h3> <h3 align="center">多云环境下更好用的堡垒机</h3>
<p align="center"> <p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a> <a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a> <a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a> <a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
<a href="https://github.com/jumpserver/jumpserver/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jumpserver/jumpserver.svg" /></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a> <a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p> </p>
@ -15,7 +18,7 @@
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。 JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。 JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。 JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
@ -28,9 +31,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 开源: 零门槛,线上快速获取和安装; - 开源: 零门槛,线上快速获取和安装;
- 分布式: 轻松支持大规模并发访问; - 分布式: 轻松支持大规模并发访问;
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验; - 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
- 多租户: 一套系统,多个子公司或部门同时使用;
- 多云支持: 一套系统,同时管理不同云上面的资产; - 多云支持: 一套系统,同时管理不同云上面的资产;
- 云端存储: 审计录像云端存储,永不丢失; - 云端存储: 审计录像云端存储,永不丢失;
- 多租户: 一套系统,多个子公司和部门同时使用;
- 多应用支持: 数据库Windows远程应用Kubernetes。 - 多应用支持: 数据库Windows远程应用Kubernetes。
### UI 展示 ### UI 展示
@ -55,12 +58,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [手动安装](https://github.com/jumpserver/installer) - [手动安装](https://github.com/jumpserver/installer)
### 组件项目 ### 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目 | 项目 | 状态 | 描述 |
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目 | --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) | [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) | [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目 | [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目 | [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
| [Installer](https://github.com/jumpserver/installer)| <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
### 社区 ### 社区
@ -75,27 +81,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
感谢以下贡献者,让 JumpServer 更加完善 感谢以下贡献者,让 JumpServer 更加完善
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"> <a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
<a href="https://github.com/jumpserver/koko/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
</a>
<a href="https://github.com/jumpserver/lina/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
</a>
<a href="https://github.com/jumpserver/luna/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
</a>
### 致谢 ### 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化组件 Lion 依赖 - [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备JumpServer 图形化组件 Lion 依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖 - [OmniDB](https://omnidb.org/) Web 页面连接使用数据库JumpServer Web 数据库依赖
### JumpServer 企业版 ### JumpServer 企业版
@ -103,14 +95,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
### 案例研究 ### 案例研究
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147) - [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882) - [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851) - [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516) - [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732) - [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708) - [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687) - [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666) - [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
### 安全说明 ### 安全说明
@ -131,4 +123,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
https://www.gnu.org/licenses/gpl-3.0.html https://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@ -47,7 +47,7 @@ class LoginAssetCheckAPI(CreateAPIView):
asset=self.serializer.asset, asset=self.serializer.asset,
system_user=self.serializer.system_user, system_user=self.serializer.system_user,
assignees=acl.reviewers.all(), assignees=acl.reviewers.all(),
org_id=self.serializer.org.id org_id=self.serializer.org.id,
) )
confirm_status_url = reverse( confirm_status_url = reverse(
view_name='api-tickets:super-ticket-status', view_name='api-tickets:super-ticket-status',
@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True external=True, api_to_ui=True
) )
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all() ticket_assignees = ticket.current_step.ticket_assignees.all()
data = { data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@ -97,32 +97,25 @@ class LoginACL(BaseACL):
return allow, reject_type return allow, reject_type
@staticmethod def create_confirm_ticket(self, request):
def construct_confirm_ticket_meta(request=None): from tickets import const
from tickets.models import ApplyLoginTicket
from orgs.models import Organization
title = _('Login confirm') + ' {}'.format(self.user)
login_ip = get_request_ip(request) if request else '' login_ip = get_request_ip(request) if request else ''
login_ip = login_ip or '0.0.0.0' login_ip = login_ip or '0.0.0.0'
login_city = get_ip_city(login_ip) login_city = get_ip_city(login_ip)
login_datetime = local_now_display() login_datetime = local_now_display()
ticket_meta = {
'apply_login_ip': login_ip,
'apply_login_city': login_city,
'apply_login_datetime': login_datetime,
}
return ticket_meta
def create_confirm_ticket(self, request=None):
from tickets import const
from tickets.models import Ticket
from orgs.models import Organization
ticket_title = _('Login confirm') + ' {}'.format(self.user)
ticket_meta = self.construct_confirm_ticket_meta(request)
data = { data = {
'title': ticket_title, 'title': title,
'type': const.TicketType.login_confirm.value, 'type': const.TicketType.login_confirm,
'meta': ticket_meta, 'applicant': self.user,
'apply_login_city': login_city,
'apply_login_ip': login_ip,
'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID, 'org_id': Organization.ROOT_ID,
} }
ticket = Ticket.objects.create(**data) ticket = ApplyLoginTicket.objects.create(**data)
ticket.create_process_map_and_node(self.reviewers.all()) assignees = self.reviewers.all()
ticket.open(self.user) ticket.open_by_system(assignees)
return ticket return ticket

View File

@ -85,19 +85,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod @classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id): def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
from tickets.const import TicketType from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import ApplyLoginAssetTicket
title = _('Login asset confirm') + ' ({})'.format(user)
data = { data = {
'title': _('Login asset confirm') + ' ({})'.format(user), 'title': title,
'type': TicketType.login_asset_confirm, 'type': TicketType.login_asset_confirm,
'meta': { 'applicant': user,
'apply_login_user': str(user), 'apply_login_user': user,
'apply_login_asset': str(asset), 'apply_login_asset': asset,
'apply_login_system_user': str(system_user), 'apply_login_system_user': system_user,
},
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = ApplyLoginAssetTicket.objects.create(**data)
ticket.create_process_map_and_node(assignees) ticket.open_by_system(assignees)
ticket.open(applicant=user)
return ticket return ticket

View File

@ -2,14 +2,16 @@
# #
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from django.db.models import F, Q from django.db.models import Q
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.drf.api import JMSBulkModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from assets.models import SystemUser from assets.models import SystemUser
from ..models import Account from ..models import Account
from ..hands import NeedMFAVerify
from .. import serializers from .. import serializers
@ -54,9 +56,9 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
perm_model = SystemUser perm_model = SystemUser
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet): class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [RBACPermission, NeedMFAVerify] permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
http_method_names = ['get', 'options'] http_method_names = ['get', 'options']
rbac_perms = { rbac_perms = {
'retrieve': 'applications.view_applicationaccountsecret', 'retrieve': 'applications.view_applicationaccountsecret',

View File

@ -1,6 +1,5 @@
# coding: utf-8 # coding: utf-8
# #
from django.shortcuts import get_object_or_404
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response

View File

@ -11,5 +11,4 @@
""" """
from common.permissions import NeedMFAVerify
from users.models import User, UserGroup from users.models import User, UserGroup

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-05-20 11:04 # Generated by Django 2.1.7 on 2019-05-20 11:04
import common.fields.model import common.db.fields
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')), ('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
('path', models.CharField(max_length=128, verbose_name='App path')), ('path', models.CharField(max_length=128, verbose_name='App path')),
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')), ('params', common.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), ('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_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),

View File

@ -1,7 +1,7 @@
# Generated by Django 3.1.12 on 2021-08-26 09:07 # Generated by Django 3.1.12 on 2021-08-26 09:07
import assets.models.base import assets.models.base
import common.fields.model import common.db.fields
from django.conf import settings from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@ -26,9 +26,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), ('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), ('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
@ -56,9 +56,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), ('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), ('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),

View File

@ -0,0 +1,22 @@
# Generated by Django 3.1.14 on 2022-06-29 10:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0020_auto_20220316_2028'),
]
operations = [
migrations.AlterModelOptions(
name='historicalaccount',
options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Application account', 'verbose_name_plural': 'historical Application accounts'},
),
migrations.AlterField(
model_name='historicalaccount',
name='history_date',
field=models.DateTimeField(db_index=True),
),
]

View File

@ -44,6 +44,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_remote_app(self): def category_remote_app(self):
return self.category == const.AppCategory.remote_app.value return self.category == const.AppCategory.remote_app.value
@property
def category_cloud(self):
return self.category == const.AppCategory.cloud.value
@property
def category_db(self):
return self.category == const.AppCategory.db.value
def get_rdp_remote_app_setting(self): def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app: if not self.category_remote_app:
@ -76,3 +84,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
if raise_exception: if raise_exception:
raise ValueError("Remote App not has asset attr") raise ValueError("Remote App not has asset attr")
def get_target_ip(self):
target_ip = ''
if self.category_remote_app:
asset = self.get_remote_app_asset()
target_ip = asset.ip if asset else target_ip
elif self.category_cloud:
target_ip = self.attrs.get('cluster')
elif self.category_db:
target_ip = self.attrs.get('host')
return target_ip

View File

@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.serializers.base import AuthSerializerMixin from assets.serializers.base import AuthSerializerMixin
from common.drf.serializers import MethodSerializer from common.drf.serializers import MethodSerializer, SecretReadableMixin
from .attrs import ( from .attrs import (
category_serializer_classes_mapping, category_serializer_classes_mapping,
type_serializer_classes_mapping, type_serializer_classes_mapping,
@ -152,7 +152,7 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
return super().to_representation(instance) return super().to_representation(instance)
class AppAccountSecretSerializer(AppAccountSerializer): class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
class Meta(AppAccountSerializer.Meta): class Meta(AppAccountSerializer.Meta):
fields_backup = [ fields_backup = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key', 'id', 'app_display', 'attrs', 'username', 'password', 'private_key',

View File

@ -1,6 +1,7 @@
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.fields import EncryptedField
from ..application_category import RemoteAppSerializer from ..application_category import RemoteAppSerializer
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer'] __all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
@ -13,19 +14,21 @@ class ChromeSerializer(RemoteAppSerializer):
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True, max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
) )
chrome_target = serializers.CharField( chrome_target = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True, max_length=128, allow_blank=True, required=False,
label=_('Target URL'), allow_null=True,
) )
chrome_username = serializers.CharField( chrome_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Chrome username'), allow_null=True, max_length=128, allow_blank=True, required=False,
label=_('Chrome username'), allow_null=True,
) )
chrome_password = serializers.CharField( chrome_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'), max_length=128, allow_blank=True, required=False,
allow_null=True label=_('Chrome password'), allow_null=True
) )
class ChromeSecretSerializer(ChromeSerializer): class ChromeSecretSerializer(ChromeSerializer):
chrome_password = serializers.CharField( chrome_password = EncryptedField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'), max_length=128, allow_blank=True, required=False,
allow_null=True label=_('Chrome password'), allow_null=True, write_only=False
) )

View File

@ -1,6 +1,7 @@
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.fields import EncryptedField
from ..application_category import RemoteAppSerializer from ..application_category import RemoteAppSerializer
__all__ = ['CustomSerializer', 'CustomSecretSerializer'] __all__ = ['CustomSerializer', 'CustomSecretSerializer']
@ -19,14 +20,14 @@ class CustomSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Custom Username'), max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
allow_null=True, allow_null=True,
) )
custom_password = serializers.CharField( custom_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Custom password'), max_length=128, allow_blank=True, required=False,
allow_null=True, label=_('Custom password'), allow_null=True,
) )
class CustomSecretSerializer(RemoteAppSerializer): class CustomSecretSerializer(RemoteAppSerializer):
custom_password = serializers.CharField( custom_password = EncryptedField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Custom password'), max_length=128, allow_blank=True, required=False, write_only=False,
allow_null=True, label=_('Custom password'), allow_null=True,
) )

View File

@ -1,6 +1,7 @@
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.fields import EncryptedField
from ..application_category import RemoteAppSerializer from ..application_category import RemoteAppSerializer
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer'] __all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
@ -29,14 +30,14 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'), max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
allow_null=True, allow_null=True,
) )
mysql_workbench_password = serializers.CharField( mysql_workbench_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Mysql workbench password'), max_length=128, allow_blank=True, required=False,
allow_null=True, label=_('Mysql workbench password'), allow_null=True,
) )
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer): class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
mysql_workbench_password = serializers.CharField( mysql_workbench_password = EncryptedField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Mysql workbench password'), max_length=128, allow_blank=True, required=False, write_only=False,
allow_null=True, label=_('Mysql workbench password'), allow_null=True,
) )

View File

@ -1,6 +1,7 @@
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.fields import EncryptedField
from ..application_category import RemoteAppSerializer from ..application_category import RemoteAppSerializer
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer'] __all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
@ -25,14 +26,14 @@ class VMwareClientSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Vmware username'), max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
allow_null=True allow_null=True
) )
vmware_password = serializers.CharField( vmware_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Vmware password'), max_length=128, allow_blank=True, required=False,
allow_null=True label=_('Vmware password'), allow_null=True
) )
class VMwareClientSecretSerializer(RemoteAppSerializer): class VMwareClientSecretSerializer(RemoteAppSerializer):
vmware_password = serializers.CharField( vmware_password = EncryptedField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Vmware password'), max_length=128, allow_blank=True, required=False, write_only=False,
allow_null=True label=_('Vmware password'), allow_null=True
) )

View File

@ -12,3 +12,4 @@ from .cmd_filter import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .account_backup import * from .account_backup import *
from .account_history import *

View File

@ -0,0 +1,39 @@
from assets.api.accounts import (
AccountFilterSet, AccountViewSet, AccountSecretsViewSet
)
from common.mixins import RecordViewLogMixin
from .. import serializers
from ..models import Account
__all__ = ['AccountHistoryViewSet', 'AccountHistorySecretsViewSet']
class AccountHistoryFilterSet(AccountFilterSet):
class Meta:
model = Account.history.model
fields = AccountFilterSet.Meta.fields
class AccountHistoryViewSet(AccountViewSet):
model = Account.history.model
filterset_class = AccountHistoryFilterSet
serializer_classes = {
'default': serializers.AccountHistorySerializer,
}
rbac_perms = {
'list': 'assets.view_assethistoryaccount',
'retrieve': 'assets.view_assethistoryaccount',
}
http_method_names = ['get', 'options']
class AccountHistorySecretsViewSet(RecordViewLogMixin, AccountHistoryViewSet):
serializer_classes = {
'default': serializers.AccountHistorySecretSerializer
}
http_method_names = ['get']
permission_classes = AccountSecretsViewSet.permission_classes
rbac_perms = {
'list': 'assets.view_assethistoryaccountsecret',
'retrieve': 'assets.view_assethistoryaccountsecret',
}

View File

@ -1,4 +1,4 @@
from django.db.models import F, Q from django.db.models import Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from rest_framework.decorators import action from rest_framework.decorators import action
@ -8,12 +8,14 @@ from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.permissions import NeedMFAVerify from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from ..tasks.account_connectivity import test_accounts_connectivity_manual from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node from ..models import Node, Account
from .. import serializers from .. import serializers
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI'] __all__ = ['AccountFilterSet', 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
class AccountFilterSet(BaseFilterSet): class AccountFilterSet(BaseFilterSet):
@ -48,16 +50,16 @@ class AccountFilterSet(BaseFilterSet):
return qs return qs
class Meta: class Meta:
model = AuthBook model = Account
fields = [ fields = [
'asset', 'systemuser', 'id', 'asset', 'id',
] ]
class AccountViewSet(OrgBulkModelViewSet): class AccountViewSet(OrgBulkModelViewSet):
model = AuthBook model = Account
filterset_fields = ("username", "asset", "systemuser", 'ip', 'hostname') filterset_fields = ("username", "asset", 'ip', 'hostname')
search_fields = ('username', 'ip', 'hostname', 'systemuser__username') search_fields = ('username', 'ip', 'hostname')
filterset_class = AccountFilterSet filterset_class = AccountFilterSet
serializer_classes = { serializer_classes = {
'default': serializers.AccountSerializer, 'default': serializers.AccountSerializer,
@ -68,10 +70,6 @@ class AccountViewSet(OrgBulkModelViewSet):
'partial_update': 'assets.change_assetaccountsecret', 'partial_update': 'assets.change_assetaccountsecret',
} }
def get_queryset(self):
queryset = AuthBook.get_queryset()
return queryset
@action(methods=['post'], detail=True, url_path='verify') @action(methods=['post'], detail=True, url_path='verify')
def verify_account(self, request, *args, **kwargs): def verify_account(self, request, *args, **kwargs):
account = super().get_object() account = super().get_object()
@ -79,7 +77,7 @@ class AccountViewSet(OrgBulkModelViewSet):
return Response(data={'task': task.id}) return Response(data={'task': task.id})
class AccountSecretsViewSet(AccountViewSet): class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
""" """
因为可能要导出所有账号所以单独建立了一个 viewset 因为可能要导出所有账号所以单独建立了一个 viewset
""" """
@ -87,7 +85,7 @@ class AccountSecretsViewSet(AccountViewSet):
'default': serializers.AccountSecretSerializer 'default': serializers.AccountSecretSerializer
} }
http_method_names = ['get'] http_method_names = ['get']
permission_classes = [RBACPermission, NeedMFAVerify] permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
rbac_perms = { rbac_perms = {
'list': 'assets.view_assetaccountsecret', 'list': 'assets.view_assetaccountsecret',
'retrieve': 'assets.view_assetaccountsecret', 'retrieve': 'assets.view_assetaccountsecret',
@ -104,7 +102,7 @@ class AccountTaskCreateAPI(CreateAPIView):
return request.user.has_perm('assets.test_assetconnectivity') return request.user.has_perm('assets.test_assetconnectivity')
def get_accounts(self): def get_accounts(self):
queryset = AuthBook.objects.all() queryset = Account.objects.all()
queryset = self.filter_queryset(queryset) queryset = self.filter_queryset(queryset)
return queryset return queryset

View File

@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
external=True, api_to_ui=True external=True, api_to_ui=True
) )
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all() ticket_assignees = ticket.current_step.ticket_assignees.all()
return { return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@ -4,7 +4,6 @@ from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.utils.crypto import get_aes_crypto
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.mixins.api import SuggestionMixin from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
@ -103,27 +102,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = SystemUserTempAuthSerializer serializer_class = SystemUserTempAuthSerializer
def decrypt_data_if_need(self, data):
csrf_token = self.request.META.get('CSRF_COOKIE')
aes = get_aes_crypto(csrf_token, 'ECB')
password = data.get('password', '')
try:
data['password'] = aes.decrypt(password)
except:
pass
return data
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = super().get_serializer(data=request.data) serializer = super().get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
pk = kwargs.get('pk') pk = kwargs.get('pk')
data = self.decrypt_data_if_need(serializer.validated_data) data = serializer.validated_data
instance_id = data.get('instance_id') asset_or_app_id = data.get('instance_id')
with tmp_to_root_org(): with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk) instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, self.request.user.id, data) instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
return Response(serializer.data, status=201) return Response(serializer.data, status=201)

View File

@ -68,7 +68,6 @@ class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
class SystemUserAssetRelationViewSet(BaseRelationViewSet): class SystemUserAssetRelationViewSet(BaseRelationViewSet):
perm_model = models.AuthBook
serializer_class = serializers.SystemUserAssetRelationSerializer serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through model = models.SystemUser.assets.through
filterset_fields = [ filterset_fields = [

View File

@ -1,7 +1,7 @@
# Generated by Django 2.1.7 on 2019-06-24 13:08 # Generated by Django 2.1.7 on 2019-06-24 13:08
import assets.models.utils import assets.models.utils
import common.fields.model import common.db.fields
from django.db import migrations from django.db import migrations
@ -15,61 +15,61 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='_password', name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='_private_key', name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='_public_key', name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='_password', name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='_private_key', name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='_public_key', name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='_password', name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='_private_key', name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='_public_key', name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='_password', name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'), field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='_private_key', name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='_public_key', name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
), ),
] ]

View File

@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-07-11 12:18 # Generated by Django 2.1.7 on 2019-07-11 12:18
import common.fields.model import common.db.fields
from django.db import migrations from django.db import migrations
@ -14,21 +14,21 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='private_key', name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='authbook', model_name='authbook',
name='private_key', name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='private_key', name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='private_key', name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'), field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
), ),
] ]

View File

@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2019-12-06 07:26 # Generated by Django 2.2.7 on 2019-12-06 07:26
import common.fields.model import common.db.fields
from django.db import migrations, models from django.db import migrations, models
@ -36,7 +36,7 @@ class Migration(migrations.Migration):
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')), ('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')), ('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')), ('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')), ('meta', common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
('internal', models.BooleanField(default=False, verbose_name='Internal')), ('internal', models.BooleanField(default=False, verbose_name='Internal')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
], ],

View File

@ -1,6 +1,6 @@
# Generated by Django 3.1.6 on 2021-06-05 16:10 # Generated by Django 3.1.6 on 2021-06-05 16:10
import common.fields.model import common.db.fields
from django.conf import settings from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
@ -58,9 +58,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')), ('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')), ('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')), ('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')), ('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')), ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),

View File

@ -0,0 +1,32 @@
# Generated by Django 3.1.14 on 2022-04-12 03:45
from django.db import migrations, models
def create_internal_platform(apps, schema_editor):
model = apps.get_model("assets", "Platform")
db_alias = schema_editor.connection.alias
type_platforms = (
('AIX', 'Unix', None),
)
for name, base, meta in type_platforms:
defaults = {'name': name, 'base': base, 'meta': meta, 'internal': True}
model.objects.using(db_alias).update_or_create(
name=name, defaults=defaults
)
class Migration(migrations.Migration):
dependencies = [
('assets', '0089_auto_20220310_0616'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
),
migrations.RunPython(create_internal_platform)
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.1.14 on 2022-06-29 10:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0090_auto_20220412_1145'),
]
operations = [
migrations.AlterModelOptions(
name='authbook',
options={'permissions': [('test_authbook', 'Can test asset account connectivity'), ('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret'), ('view_assethistoryaccount', 'Can view asset history account'), ('view_assethistoryaccountsecret', 'Can view asset history account secret')], 'verbose_name': 'AuthBook'},
),
migrations.AlterModelOptions(
name='historicalauthbook',
options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical AuthBook', 'verbose_name_plural': 'historical AuthBooks'},
),
migrations.AlterField(
model_name='historicalauthbook',
name='history_date',
field=models.DateTimeField(db_index=True),
),
]

View File

@ -0,0 +1,83 @@
# Generated by Django 3.2.12 on 2022-07-11 08:59
import assets.models.base
import assets.models.user
import common.db.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0091_auto_20220629_1826'),
]
operations = [
migrations.CreateModel(
name='HistoricalAccount',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('protocol', models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol')),
('type', models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('asset', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.asset', verbose_name='Asset')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical Account',
'verbose_name_plural': 'historical Accounts',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Account',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity')),
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('protocol', models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol')),
('type', models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type')),
('version', models.IntegerField(default=0, verbose_name='Version')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.asset', verbose_name='Asset')),
],
options={
'verbose_name': 'Account',
'permissions': [('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret'), ('view_assethistoryaccount', 'Can view asset history account'), ('view_assethistoryaccountsecret', 'Can view asset history account secret')],
'unique_together': {('username', 'asset')},
},
bases=(models.Model, assets.models.base.AuthMixin, assets.models.user.ProtocolMixin),
),
]

View File

@ -0,0 +1,65 @@
# Generated by Django 3.2.12 on 2022-07-11 06:13
import time
from django.db import migrations
def migrate_accounts(apps, schema_editor):
auth_book_model = apps.get_model('assets', 'AuthBook')
account_model = apps.get_model('assets', 'Account')
count = 0
bulk_size = 1000
print("\nStart migrate accounts")
while True:
start = time.time()
auth_books = auth_book_model.objects \
.prefetch_related('systemuser') \
.all()[count:count+bulk_size]
count += len(auth_books)
if not auth_books:
break
accounts = []
# auth book 和 account 相同的属性
same_attrs = [
'id', 'comment', 'date_created', 'date_updated',
'created_by', 'asset_id', 'org_id',
]
# 认证的属性,可能是 authbook 的,可能是 systemuser 的
auth_attrs = ['username', 'password', 'private_key', 'public_key']
for auth_book in auth_books:
values = {attr: getattr(auth_book, attr) for attr in same_attrs}
values['protocol'] = 'ssh'
values['version'] = 1
system_user = auth_book.systemuser
if auth_book.systemuser:
values.update({attr: getattr(system_user, attr) for attr in auth_attrs})
values['protocol'] = system_user.protocol
values['created_by'] = str(system_user.id)
values['type'] = system_user.type
auth_book_auth = {attr: getattr(auth_book, attr) for attr in auth_attrs}
auth_book_auth = {attr: value for attr, value in auth_book_auth.items() if value}
values.update(auth_book_auth)
account = account_model(**values)
accounts.append(account)
account_model.objects.bulk_create(accounts, ignore_conflicts=True)
print("Create accounts: {}-{} using: {:.2f}s".format(
count - bulk_size, count, time.time()-start
))
class Migration(migrations.Migration):
dependencies = [
('assets', '0092_auto_20220711_1409'),
]
operations = [
migrations.RunPython(migrate_accounts)
]

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.12 on 2022-07-13 09:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0093_auto_20220711_1413'),
]
operations = [
migrations.RemoveField(
model_name='systemuser',
name='assets',
),
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, related_name='system_users', to='assets.Asset', verbose_name='Assets'),
),
]

View File

@ -0,0 +1,45 @@
# Generated by Django 3.2.12 on 2022-07-13 09:46
import time
from django.db import migrations
def migrate_asset_system_user_relations(apps, schema_editor):
system_user_model = apps.get_model('assets', 'SystemUser')
old_model = apps.get_model('assets', 'AuthBook')
new_model = system_user_model.assets.through
count = 0
bulk_size = 1000
print("\nStart migrate asset system user relations")
while True:
start = time.time()
auth_books = old_model.objects.only('asset_id', 'systemuser_id')[count:count+bulk_size]
auth_books = list(auth_books)
count += len(auth_books)
if not auth_books:
break
asset_system_users = []
for auth_book in auth_books:
if not auth_book.asset_id or not auth_book.systemuser_id:
continue
asset_system_user = new_model(
asset_id=auth_book.asset_id,
systemuser_id=auth_book.systemuser_id
)
asset_system_users.append(asset_system_user)
new_model.objects.bulk_create(asset_system_users, ignore_conflicts=True)
print("Create asset system user relations: {}-{} using: {:.2f}s".format(
count - bulk_size, count, time.time()-start
))
class Migration(migrations.Migration):
dependencies = [
('assets', '0094_alter_systemuser_assets'),
]
operations = [
migrations.RunPython(migrate_asset_system_user_relations)
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2022-07-14 08:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0095_auto_20220713_1746'),
]
operations = [
migrations.RenameField(
model_name='systemuser',
old_name='auto_push',
new_name='auto_push_account',
),
migrations.AddField(
model_name='systemuser',
name='account_template_enabled',
field=models.BooleanField(default=False, verbose_name='Auto account if not exist'),
),
]

View File

@ -14,3 +14,4 @@ from .authbook import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .backup import * from .backup import *
from .account import *

View File

@ -0,0 +1,35 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
from .user import ProtocolMixin
from .base import BaseUser, AbsConnectivity
__all__ = ['Account']
class Account(BaseUser, AbsConnectivity, ProtocolMixin):
class Type(models.TextChoices):
common = 'common', _('Common user')
admin = 'admin', _('Admin user')
protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices,
default='ssh', verbose_name=_('Protocol'))
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_("Type"))
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = HistoricalRecords()
class Meta:
verbose_name = _('Account')
unique_together = [('username', 'asset')]
permissions = [
('view_assetaccountsecret', _('Can view asset account secret')),
('change_assetaccountsecret', _('Can change asset account secret')),
('view_assethistoryaccount', _('Can view asset history account')),
('view_assethistoryaccountsecret', _('Can view asset history account secret')),
]
def __str__(self):
return '{}://{}@{}'.format(self.protocol, self.username, self.asset.hostname)

View File

@ -11,6 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from common.db.fields import JsonDictTextField
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from assets.const import Category, AllTypes from assets.const import Category, AllTypes
@ -147,7 +148,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
# Some information # Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) number = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Asset number'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
@ -159,15 +160,8 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def __str__(self): def __str__(self):
return '{0.hostname}({0.ip})'.format(self) return '{0.hostname}({0.ip})'.format(self)
def set_admin_user_relation(self): def get_target_ip(self):
from assets.models import AuthBook return self.ip
if not self.admin_user:
return
if self.admin_user.type != 'admin':
raise ValidationError('System user should be type admin')
defaults = {'asset': self, 'systemuser': self.admin_user, 'org_id': self.org_id}
AuthBook.objects.get_or_create(defaults=defaults, asset=self, systemuser=self.admin_user)
@property @property
def admin_user_display(self): def admin_user_display(self):
@ -222,7 +216,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
'private_key': auth_user.private_key_file 'private_key': auth_user.private_key_file
} }
if not with_become: if not with_become or self.is_windows():
return info return info
if become_user: if become_user:

View File

@ -29,7 +29,9 @@ class AuthBook(BaseUser, AbsConnectivity):
permissions = [ permissions = [
('test_authbook', _('Can test asset account connectivity')), ('test_authbook', _('Can test asset account connectivity')),
('view_assetaccountsecret', _('Can view asset account secret')), ('view_assetaccountsecret', _('Can view asset account secret')),
('change_assetaccountsecret', _('Can change asset account secret')) ('change_assetaccountsecret', _('Can change asset account secret')),
('view_assethistoryaccount', _('Can view asset history account')),
('view_assethistoryaccountsecret', _('Can view asset history account secret')),
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -123,8 +125,9 @@ class AuthBook(BaseUser, AbsConnectivity):
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser)) logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
@classmethod @classmethod
def get_queryset(cls): def get_queryset(cls, is_history_model=False):
queryset = cls.objects.all() \ model = cls.history.model if is_history_model else cls
queryset = model.objects.all() \
.annotate(ip=F('asset__ip')) \ .annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname')) \ .annotate(hostname=F('asset__hostname')) \
.annotate(platform=F('asset__platform__name')) \ .annotate(platform=F('asset__platform__name')) \
@ -134,3 +137,4 @@ class AuthBook(BaseUser, AbsConnectivity):
def __str__(self): def __str__(self):
return self.smart_name return self.smart_name

View File

@ -19,7 +19,7 @@ from common.utils import (
) )
from common.utils.encode import ssh_pubkey_gen from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric from common.validators import alphanumeric
from common import fields from common.db import fields
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin

View File

@ -165,24 +165,23 @@ class CommandFilterRule(OrgModelMixin):
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import ApplyCommandTicket
data = { data = {
'title': _('Command confirm') + ' ({})'.format(session.user), 'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm, 'type': TicketType.command_confirm,
'meta': { 'applicant': session.user_obj,
'apply_run_user': session.user, 'apply_run_user_id': session.user_id,
'apply_run_asset': session.asset, 'apply_run_asset': str(session.asset),
'apply_run_system_user': session.system_user, 'apply_run_system_user_id': session.system_user_id,
'apply_run_command': run_command, 'apply_run_command': run_command[:4090],
'apply_from_session_id': str(session.id), 'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id) 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
},
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = ApplyCommandTicket.objects.create(**data)
ticket.create_process_map_and_node(self.reviewers.all()) assignees = self.reviewers.all()
ticket.open(applicant=session.user_obj) ticket.open_by_system(assignees)
return ticket return ticket
@classmethod @classmethod

View File

@ -9,7 +9,7 @@ import paramiko
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.utils import get_logger from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .base import BaseUser from .base import BaseUser
@ -36,7 +36,7 @@ class Domain(OrgModelMixin):
def has_gateway(self): def has_gateway(self):
return self.gateway_set.filter(is_active=True).exists() return self.gateway_set.filter(is_active=True).exists()
@property @lazyproperty
def gateways(self): def gateways(self):
return self.gateway_set.filter(is_active=True) return self.gateway_set.filter(is_active=True)
@ -44,8 +44,9 @@ class Domain(OrgModelMixin):
gateways = [gw for gw in self.gateways if gw.is_connective] gateways = [gw for gw in self.gateways if gw.is_connective]
if gateways: if gateways:
return random.choice(gateways) return random.choice(gateways)
else:
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.') logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
if self.gateways:
return random.choice(self.gateways) return random.choice(self.gateways)

View File

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from assets.const import Category, AllTypes from assets.const import Category, AllTypes
from common.fields.model import JsonDictTextField from common.db.fields import JsonDictTextField
__all__ = ['Platform'] __all__ = ['Platform']

View File

@ -7,16 +7,14 @@ import logging
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 django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache
from common.utils import signer, get_object_or_none
from assets.const import Protocol from assets.const import Protocol
from common.utils import signer
from .base import BaseUser from .base import BaseUser
from .asset import Asset from .asset import Asset
from .authbook import AuthBook
__all__ = ['AdminUser', 'SystemUser'] __all__ = ['AdminUser', 'SystemUser', 'ProtocolMixin']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -70,151 +68,11 @@ class ProtocolMixin:
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
class AuthMixin: class SystemUser(ProtocolMixin, BaseUser):
username_same_with_user: bool
protocol: str
ASSET_CATEGORY_PROTOCOLS: list
login_mode: str
LOGIN_MANUAL: str
id: str
username: str
password: str
private_key: str
public_key: str
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
if not auth:
raise ValueError('Auth not set')
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Set system user temp auth: {key}')
cache.set(key, auth, ttl)
def get_temp_auth(self, asset_or_app_id, user_id):
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Get system user temp auth: {key}')
password = cache.get(key)
return password
def _clean_auth_info_if_manual_login_mode(self):
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
self.public_key = ''
def _load_tmp_auth_if_has(self, asset_or_app_id, user_id):
if self.login_mode != self.LOGIN_MANUAL:
return
if not asset_or_app_id or not user_id:
return
auth = self.get_temp_auth(asset_or_app_id, user_id)
if not auth:
return
username = auth.get('username')
password = auth.get('password')
if username:
self.username = username
if password:
self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
from applications.models import Application
app = get_object_or_none(Application, pk=app_id)
if app and app.category_remote_app:
# Remote app
self._load_remoteapp_more_auth(app, username, user_id)
return
# Other app
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# 更新用户名
from users.models import User
user = get_object_or_none(User, pk=user_id) if user_id else None
if self.username_same_with_user:
if user and not username:
_username = user.username
else:
_username = username
self.username = _username
def _load_remoteapp_more_auth(self, app, username, user_id):
asset = app.get_remote_app_asset(raise_exception=False)
if asset:
self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id)
def load_asset_special_auth(self, asset, username=''):
"""
AuthBook 的数据状态
| asset | systemuser | username |
1 | * | * | x |
2 | * | x | * |
当前 AuthBook 只有以上两种状态systemuser username 不会并存
正常的资产与系统用户关联产生的是第1种状态改密则产生第2种状态改密之后
只有 username 而没有 systemuser
Freq: 关联同一资产的多个系统用户指定同一用户名时修改用户密码会影响所有系统用户
这里有一个不对称的行为同名系统用户密码覆盖
当有相同 username 的多个系统用户时有改密动作之后所有的同名系统用户都使用最后
一次改动但如果没有发生过改密同名系统用户使用的密码还是各自的
"""
if username == '':
username = self.username
authbook = AuthBook.objects.filter(
asset=asset, username=username, systemuser__isnull=True
).order_by('-date_created').first()
if not authbook:
authbook = AuthBook.objects.filter(
asset=asset, systemuser=self
).order_by('-date_created').first()
if not authbook:
return None
authbook.load_auth()
self.password = authbook.password
self.private_key = authbook.private_key
self.public_key = authbook.public_key
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
from users.models import User
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(asset_id, user_id)
return
# 更新用户名
user = get_object_or_none(User, pk=user_id) if user_id else None
if self.username_same_with_user:
if user and not username:
_username = user.username
else:
_username = username
self.username = _username
# 加载某个资产的特殊配置认证信息
asset = get_object_or_none(Asset, pk=asset_id) if asset_id else None
if not asset:
logger.debug('Asset not found, pass')
return
self.load_asset_special_auth(asset, self.username)
class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
LOGIN_AUTO = 'auto' LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual' LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = ( LOGIN_MODE_CHOICES = (
(LOGIN_AUTO, _('Automatic managed')), (LOGIN_AUTO, _('使用账号')),
(LOGIN_MANUAL, _('Manually input')) (LOGIN_MANUAL, _('Manually input'))
) )
@ -226,24 +84,34 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField( assets = models.ManyToManyField(
'assets.Asset', blank=True, verbose_name=_("Assets"), 'assets.Asset', blank=True, verbose_name=_("Assets"),
through='assets.AuthBook', through_fields=['systemuser', 'asset'],
related_name='system_users' related_name='system_users'
) )
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users")) users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups")) groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type')) priority = models.IntegerField(
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) default=81, verbose_name=_("Priority"),
help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]
)
protocol = models.CharField(max_length=16, choices=Protocol.choices, default='ssh', verbose_name=_('Protocol')) protocol = models.CharField(max_length=16, choices=Protocol.choices, default='ssh', verbose_name=_('Protocol'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
# Todo: 重构平台后或许这里也得变化
# 账号模版
account_template_enabled = models.BooleanField(default=False, verbose_name=_("启用账号模版"))
auto_push_account = models.BooleanField(default=True, verbose_name=_('自动推送账号'))
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root")) sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token')) token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True) home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True) system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256) ad_domain = models.CharField(default='', max_length=256)
# linux su 命令 (switch user) # linux su 命令 (switch user)
# Todo: 修改为 username, 不必系统用户了
su_enabled = models.BooleanField(default=False, verbose_name=_('User switch')) su_enabled = models.BooleanField(default=False, verbose_name=_('User switch'))
su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from")) su_from = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='su_to', null=True, verbose_name=_("Switch from"))
@ -262,7 +130,7 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
return self.get_login_mode_display() return self.get_login_mode_display()
def is_need_push(self): def is_need_push(self):
if self.auto_push and self.is_protocol_support_push: if self.auto_push_account and self.is_protocol_support_push:
return True return True
else: else:
return False return False

View File

@ -11,5 +11,6 @@ from .cmd_filter import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *
from .account import * from .account import *
from .platform import * from .account_history import *
from .backup import * from .backup import *
from .platform import *

View File

@ -1,19 +1,19 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import F
from assets.models import AuthBook from assets.models import Account
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
from .utils import validate_password_contains_left_double_curly_bracket
from common.utils.encode import ssh_pubkey_gen from common.utils.encode import ssh_pubkey_gen
from common.drf.serializers import SecretReadableMixin
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
ip = serializers.ReadOnlyField(label=_("IP")) ip = serializers.ReadOnlyField(label=_("IP"))
hostname = serializers.ReadOnlyField(label=_("Hostname")) hostname = serializers.ReadOnlyField(label=_("Hostname"))
platform = serializers.ReadOnlyField(label=_("Platform")) platform = serializers.ReadOnlyField(label=_("Platform"))
protocols = serializers.SerializerMethodField(label=_("Protocols"))
date_created = serializers.DateTimeField( date_created = serializers.DateTimeField(
label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True
) )
@ -22,22 +22,20 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
) )
class Meta: class Meta:
model = AuthBook model = Account
fields_mini = ['id', 'username', 'ip', 'hostname', 'platform', 'protocols', 'version'] fields_mini = [
fields_write_only = ['password', 'private_key', "public_key", 'passphrase'] 'id', 'type', 'username', 'ip', 'hostname',
'platform', 'protocol', 'version'
]
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase']
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment'] fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
fields_small = fields_mini + fields_write_only + fields_other fields_small = fields_mini + fields_write_only + fields_other
fields_fk = ['asset', 'systemuser', 'systemuser_display'] fields_fk = ['asset']
fields = fields_small + fields_fk fields = fields_small + fields_fk
extra_kwargs = { extra_kwargs = {
'username': {'required': True}, 'username': {'required': True},
'password': {
'write_only': True,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'private_key': {'write_only': True}, 'private_key': {'write_only': True},
'public_key': {'write_only': True}, 'public_key': {'write_only': True},
'systemuser_display': {'label': _('System user display')}
} }
ref_name = 'AssetAccountSerializer' ref_name = 'AssetAccountSerializer'
@ -56,21 +54,16 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
attrs = self._validate_gen_key(attrs) attrs = self._validate_gen_key(attrs)
return attrs return attrs
def get_protocols(self, v):
return v.protocols.replace(' ', ', ')
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('systemuser', 'asset') queryset = queryset.prefetch_related('asset')\
.annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname'))
return queryset return queryset
def to_representation(self, instance):
instance.load_auth()
return super().to_representation(instance)
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountSecretSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
fields_backup = [ fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password', 'hostname', 'ip', 'platform', 'protocols', 'username', 'password',

View File

@ -0,0 +1,29 @@
from assets.models import Account
from common.drf.serializers import SecretReadableMixin
from .account import AccountSerializer, AccountSecretSerializer
class AccountHistorySerializer(AccountSerializer):
class Meta:
model = Account.history.model
fields = AccountSerializer.Meta.fields_mini + \
AccountSerializer.Meta.fields_write_only + \
AccountSerializer.Meta.fields_fk + \
['history_id', 'date_created', 'date_updated']
read_only_fields = fields
ref_name = 'AccountHistorySerializer'
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields = list(set(fields) - {'org_name'})
return fields
def to_representation(self, instance):
return super(AccountSerializer, self).to_representation(instance)
class AccountHistorySecretSerializer(SecretReadableMixin, AccountHistorySerializer):
class Meta(AccountHistorySerializer.Meta):
extra_kwargs = AccountSecretSerializer.Meta.extra_kwargs

View File

@ -3,9 +3,11 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from ...models import Asset, Node, Platform, SystemUser from ...models import Asset, Node, Platform, SystemUser
from ..mixin import CategoryDisplayMixin from ..mixin import CategoryDisplayMixin
from ..account import AccountSerializer
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
@ -76,6 +78,7 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
platform_display = serializers.SlugField( platform_display = serializers.SlugField(
source='platform.name', label=_("Platform display"), read_only=True source='platform.name', label=_("Platform display"), read_only=True
) )
accounts = AccountSerializer(many=True, write_only=True, required=False)
""" """
资产的数据结构 资产的数据结构
@ -95,7 +98,7 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
'admin_user', 'admin_user_display' 'admin_user', 'admin_user_display'
] ]
fields_m2m = [ fields_m2m = [
'nodes', 'nodes_display', 'labels', 'labels_display', 'nodes', 'nodes_display', 'labels', 'labels_display', 'accounts'
] ]
read_only_fields = [ read_only_fields = [
'category', 'category_display', 'type', 'type_display', 'category', 'category_display', 'type', 'type_display',
@ -110,6 +113,11 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
'admin_user_display': {'label': _('Admin user display'), 'read_only': True}, 'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
} }
def __init__(self, *args, **kwargs):
data = kwargs.get('data', {})
self.accounts_data = data.pop('accounts', [])
super().__init__(*args, **kwargs)
def get_fields(self): def get_fields(self):
fields = super().get_fields() fields = super().get_fields()
@ -161,10 +169,24 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
nodes_to_set.append(node) nodes_to_set.append(node)
instance.nodes.set(nodes_to_set) instance.nodes.set(nodes_to_set)
@staticmethod
def add_accounts(instance, accounts_data):
for data in accounts_data:
data['asset'] = instance.id
print("Data: ", accounts_data)
serializer = AccountSerializer(data=accounts_data, many=True)
try:
serializer.is_valid(raise_exception=True)
except Exception as e:
raise serializers.ValidationError({'accounts': e})
serializer.save()
def create(self, validated_data): def create(self, validated_data):
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
nodes_display = validated_data.pop('nodes_display', '') nodes_display = validated_data.pop('nodes_display', '')
instance = super().create(validated_data) instance = super().create(validated_data)
if self.accounts_data:
self.add_accounts(instance, self.accounts_data)
self.perform_nodes_display_create(instance, nodes_display) self.perform_nodes_display_create(instance, nodes_display)
return instance return instance

View File

@ -6,12 +6,14 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
from common.drf.fields import EncryptedField
from assets.models import Type from assets.models import Type
from .utils import validate_password_for_ansible
class AuthSerializer(serializers.ModelSerializer): class AuthSerializer(serializers.ModelSerializer):
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024) password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096) private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key'))
def gen_keys(self, private_key=None, password=None): def gen_keys(self, private_key=None, password=None):
if private_key is None: if private_key is None:
@ -31,6 +33,13 @@ class AuthSerializer(serializers.ModelSerializer):
class AuthSerializerMixin(serializers.ModelSerializer): class AuthSerializerMixin(serializers.ModelSerializer):
password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
validators=[validate_password_for_ansible]
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
)
passphrase = serializers.CharField( passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512, allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Key password') write_only=True, label=_('Key password')

View File

@ -31,24 +31,24 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
type_display = serializers.ReadOnlyField(source='get_type_display') type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
action_display = serializers.ReadOnlyField(source='get_action_display') action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display"))
class Meta: class Meta:
model = CommandFilterRule model = CommandFilterRule
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'type', 'type_display', 'content', 'ignore_case', 'pattern', 'priority', 'type', 'type_display', 'content', 'ignore_case', 'pattern',
'action', 'action_display', 'reviewers', 'priority', 'action', 'action_display', 'reviewers',
'date_created', 'date_updated', 'date_created', 'date_updated', 'comment', 'created_by',
'comment', 'created_by',
] ]
fields_fk = ['filter'] fields_fk = ['filter']
fields = fields_small + fields_fk fields = fields_small + fields_fk
extra_kwargs = { extra_kwargs = {
'date_created': {'label': _("Date created")}, 'date_created': {'label': _("Date created")},
'date_updated': {'label': _("Date updated")}, 'date_updated': {'label': _("Date updated")},
'action_display': {'label': _("Action display")} 'action_display': {'label': _("Action display")},
'pattern': {'label': _("Pattern")}
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from common.validators import alphanumeric from common.validators import alphanumeric
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import SecretReadableMixin
from ..models import Domain, Gateway from ..models import Domain, Gateway
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
@ -43,7 +44,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
is_connective = serializers.BooleanField(required=False) is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
class Meta: class Meta:
model = Gateway model = Gateway
@ -67,7 +68,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
} }
class GatewayWithAuthSerializer(GatewaySerializer): class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
class Meta(GatewaySerializer.Meta): class Meta(GatewaySerializer.Meta):
extra_kwargs = { extra_kwargs = {
'password': {'write_only': False}, 'password': {'write_only': False},

View File

@ -4,11 +4,13 @@ from django.db.models import Count
from common.mixins.serializers import BulkSerializerMixin from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen from common.utils import ssh_pubkey_gen
from common.drf.fields import EncryptedField
from common.drf.serializers import SecretReadableMixin
from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.const import Protocol from assets.const import Protocol
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .utils import validate_password_contains_left_double_curly_bracket from .utils import validate_password_for_ansible
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
__all__ = [ __all__ = [
@ -24,9 +26,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
""" """
系统用户 系统用户
""" """
password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
trim_whitespace=False, validators=[validate_password_for_ansible],
write_only=True
)
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True) auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint')) ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint'))
token = EncryptedField(
label=_('Token'), required=False, write_only=True, style={'base_template': 'textarea.html'}
)
applications_amount = serializers.IntegerField( applications_amount = serializers.IntegerField(
source='apps_amount', read_only=True, label=_('Apps amount') source='apps_amount', read_only=True, label=_('Apps amount')
) )
@ -38,24 +48,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
'token', 'ssh_key_fingerprint', 'token', 'ssh_key_fingerprint',
'type', 'type_display', 'protocol', 'is_asset_protocol', 'type', 'type_display', 'protocol', 'is_asset_protocol',
'login_mode', 'login_mode_display', 'priority', 'account_template_enabled', 'login_mode', 'login_mode_display', 'priority',
'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain', 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain',
'username_same_with_user', 'auto_push', 'auto_generate_key', 'username_same_with_user', 'auto_push_account', 'auto_generate_key',
'su_enabled', 'su_from', 'su_enabled', 'su_from',
'date_created', 'date_updated', 'comment', 'created_by', 'date_created', 'date_updated', 'comment', 'created_by',
] ]
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes'] fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
extra_kwargs = { extra_kwargs = {
'password': {
"write_only": True,
'trim_whitespace': False,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'cmd_filters': {"required": False, 'label': _('Command filter')}, 'cmd_filters': {"required": False, 'label': _('Command filter')},
'public_key': {"write_only": True}, 'public_key': {"write_only": True},
'private_key': {"write_only": True}, 'private_key': {"write_only": True},
'token': {"write_only": True},
'nodes_amount': {'label': _('Nodes amount')}, 'nodes_amount': {'label': _('Nodes amount')},
'assets_amount': {'label': _('Assets amount')}, 'assets_amount': {'label': _('Assets amount')},
'login_mode_display': {'label': _('Login mode display')}, 'login_mode_display': {'label': _('Login mode display')},
@ -188,7 +192,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
attrs['protocol'] = Protocol.ssh attrs['protocol'] = Protocol.ssh
attrs['login_mode'] = SystemUser.LOGIN_AUTO attrs['login_mode'] = SystemUser.LOGIN_AUTO
attrs['username_same_with_user'] = False attrs['username_same_with_user'] = False
attrs['auto_push'] = False attrs['auto_push_account'] = False
return attrs return attrs
def _validate_gen_key(self, attrs): def _validate_gen_key(self, attrs):
@ -249,14 +253,14 @@ class MiniSystemUserSerializer(serializers.ModelSerializer):
fields = SystemUserSerializer.Meta.fields_mini fields = SystemUserSerializer.Meta.fields_mini
class SystemUserWithAuthInfoSerializer(SystemUserSerializer): class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer):
class Meta(SystemUserSerializer.Meta): class Meta(SystemUserSerializer.Meta):
fields_mini = ['id', 'name', 'username'] fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key'] fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
'protocol', 'login_mode', 'login_mode_display', 'priority', 'protocol', 'login_mode', 'login_mode_display', 'priority',
'sudo', 'shell', 'ad_domain', 'sftp_root', 'token', 'sudo', 'shell', 'ad_domain', 'sftp_root', 'token',
"username_same_with_user", 'auto_push', 'auto_generate_key', "username_same_with_user", 'auto_push_account', 'auto_generate_key',
'comment', 'comment',
] ]
fields = fields_small fields = fields_small
@ -265,6 +269,9 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
'assets_amount': {'label': _('Asset')}, 'assets_amount': {'label': _('Asset')},
'login_mode_display': {'label': _('Login mode display')}, 'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True}, 'created_by': {'read_only': True},
'password': {'write_only': False},
'private_key': {'write_only': False},
'token': {'write_only': False}
} }
@ -292,7 +299,7 @@ class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializ
asset_display = serializers.ReadOnlyField(label=_('Asset hostname')) asset_display = serializers.ReadOnlyField(label=_('Asset hostname'))
class Meta: class Meta:
model = SystemUser.assets.through model = SystemUser
fields = [ fields = [
"id", "asset", "asset_display", 'systemuser', 'systemuser_display', "id", "asset", "asset_display", 'systemuser', 'systemuser_display',
"connectivity", 'date_verified', 'org_id' "connectivity", 'date_verified', 'org_id'

View File

@ -2,8 +2,16 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
def validate_password_contains_left_double_curly_bracket(password): def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """
# validate password contains left double curly bracket # validate password contains left double curly bracket
# check password not contains `{{` # check password not contains `{{`
# Ansible 推送的时候不支持
if '{{' in password: if '{{' in password:
raise serializers.ValidationError(_('Password can not contains `{{` ')) raise serializers.ValidationError(_('Password can not contains `{{` '))
# Ansible Windows 推送的时候不支持
if "'" in password:
raise serializers.ValidationError(_("Password can not contains `'` "))
if '"' in password:
raise serializers.ValidationError(_('Password can not contains `"` '))

View File

@ -52,8 +52,6 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
if not has_node: if not has_node:
instance.nodes.add(Node.org_root()) instance.nodes.add(Node.org_root())
instance.set_admin_user_relation()
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs): def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
@ -89,22 +87,22 @@ def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
systemuser_id__in=system_user_ids, asset_id__in=asset_ids systemuser_id__in=system_user_ids, asset_id__in=asset_ids
).values_list('systemuser_id', 'asset_id')) ).values_list('systemuser_id', 'asset_id'))
# TODO 优化 # TODO 优化
to_create = [] # to_create = []
for system_user_id in system_user_ids: # for system_user_id in system_user_ids:
asset_ids_to_push = [] # asset_ids_to_push = []
for asset_id in asset_ids: # for asset_id in asset_ids:
if (system_user_id, asset_id) in exist: # if (system_user_id, asset_id) in exist:
continue # continue
asset_ids_to_push.append(asset_id) # asset_ids_to_push.append(asset_id)
to_create.append(m2m_model( # to_create.append(m2m_model(
systemuser_id=system_user_id, # systemuser_id=system_user_id,
asset_id=asset_id, # asset_id=asset_id,
org_id=instance.org_id # org_id=instance.org_id
)) # ))
if asset_ids_to_push: # if asset_ids_to_push:
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push) # push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
m2m_model.objects.bulk_create(to_create) # m2m_model.objects.bulk_create(to_create)
#
RELATED_NODE_IDS = '_related_node_ids' RELATED_NODE_IDS = '_related_node_ids'

View File

@ -1,50 +1,14 @@
from django.dispatch import receiver from django.dispatch import receiver
from django.apps import apps from django.db.models.signals import pre_save
from simple_history.signals import pre_create_historical_record
from django.db.models.signals import post_save, pre_save, pre_delete
from common.utils import get_logger from common.utils import get_logger
from ..models import AuthBook, SystemUser from ..models import Account
AuthBookHistory = apps.get_model('assets', 'HistoricalAuthBook')
logger = get_logger(__name__) logger = get_logger(__name__)
@receiver(pre_create_historical_record, sender=AuthBookHistory) @receiver(pre_save, sender=Account)
def pre_create_historical_record_callback(sender, history_instance=None, **kwargs): def on_account_pre_create(sender, instance, **kwargs):
attrs_to_copy = ['username', 'password', 'private_key']
for attr in attrs_to_copy:
if getattr(history_instance, attr):
continue
try:
system_user = history_instance.systemuser
except SystemUser.DoesNotExist:
continue
if not system_user:
continue
system_user_attr_value = getattr(history_instance.systemuser, attr)
if system_user_attr_value:
setattr(history_instance, attr, system_user_attr_value)
@receiver(pre_delete, sender=AuthBook)
def on_authbook_post_delete(sender, instance, **kwargs):
instance.remove_asset_admin_user_if_need()
@receiver(post_save, sender=AuthBook)
def on_authbook_post_create(sender, instance, created, **kwargs):
instance.sync_to_system_user_account()
if created:
pass
# # 不再自动更新资产管理用户,只允许用户手动指定。
# 只在创建时进行更新资产的管理用户
# instance.update_asset_admin_user_if_need()
@receiver(pre_save, sender=AuthBook)
def on_authbook_pre_create(sender, instance, **kwargs):
# 升级版本号 # 升级版本号
instance.version += 1 instance.version += 1
# 即使在 root 组织也不怕 # 即使在 root 组织也不怕

View File

@ -9,9 +9,8 @@ from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD from common.const.signals import POST_ADD
from common.utils import get_logger from common.utils import get_logger
from common.decorator import on_transaction_commit from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node, AuthBook from assets.models import Asset, SystemUser, Node
from users.models import User from users.models import User
from orgs.utils import tmp_to_root_org
from assets.tasks import ( from assets.tasks import (
push_system_user_to_assets_manual, push_system_user_to_assets_manual,
push_system_user_to_assets, push_system_user_to_assets,
@ -39,35 +38,7 @@ def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
else: else:
system_user_ids = pk_set system_user_ids = pk_set
asset_ids = [instance.id] asset_ids = [instance.id]
# todo: Auto create account if need
org_id = instance.org_id
# 关联创建的 authbook 没有系统用户id
with tmp_to_root_org():
authbooks = AuthBook.objects.filter(
asset_id__in=asset_ids,
systemuser_id__in=system_user_ids
)
if action == POST_ADD:
authbooks.update(org_id=org_id)
save_action_mapper = {
'pre_add': pre_save,
'post_add': post_save,
'pre_remove': pre_delete,
'post_remove': post_delete
}
for ab in authbooks:
ab.org_id = org_id
save_action = save_action_mapper[action]
logger.debug('Send AuthBook post save signal: {} -> {}'.format(action, ab.id))
save_action.send(sender=AuthBook, instance=ab, created=True)
if action == POST_ADD:
for system_user_id in system_user_ids:
push_system_user_to_assets.delay(system_user_id, asset_ids)
@receiver(m2m_changed, sender=SystemUser.users.through) @receiver(m2m_changed, sender=SystemUser.users.through)

View File

@ -1,13 +1,13 @@
import os import os
import time import time
import pandas as pd from openpyxl import Workbook
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
from django.conf import settings from django.conf import settings
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 assets.models import AuthBook from assets.models import Account
from assets.serializers import AccountSecretSerializer from assets.serializers import AccountSecretSerializer
from assets.notifications import AccountBackupExecutionTaskMsg from assets.notifications import AccountBackupExecutionTaskMsg
from applications.models import Account from applications.models import Account
@ -48,7 +48,7 @@ class BaseAccountHandler:
_fields = cls.get_header_fields(v) _fields = cls.get_header_fields(v)
header_fields.update(_fields) header_fields.update(_fields)
else: else:
header_fields[field] = v.label header_fields[field] = str(v.label)
return header_fields return header_fields
@classmethod @classmethod
@ -59,7 +59,7 @@ class BaseAccountHandler:
data = cls.unpack_data(serializer.data) data = cls.unpack_data(serializer.data)
row_dict = {} row_dict = {}
for field, header_name in header_fields.items(): for field, header_name in header_fields.items():
row_dict[header_name] = data[field] row_dict[header_name] = str(data[field])
return row_dict return row_dict
@ -72,24 +72,24 @@ class AssetAccountHandler(BaseAccountHandler):
return filename return filename
@classmethod @classmethod
def create_df(cls): def create_data_map(cls):
df_dict = defaultdict(list) data_map = defaultdict(list)
sheet_name = AuthBook._meta.verbose_name sheet_name = Account._meta.verbose_name
accounts = AuthBook.get_queryset().select_related('systemuser') accounts = Account.get_queryset()
if not accounts.first(): if not accounts.first():
return df_dict return data_map
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first())) header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
for account in accounts: for account in accounts:
account.load_auth() account.load_auth()
row = cls.create_row(account, AccountSecretSerializer, header_fields) row = cls.create_row(account, AccountSecretSerializer, header_fields)
df_dict[sheet_name].append(row) if sheet_name not in data_map:
for k, v in df_dict.items(): data_map[sheet_name].append(list(row.keys()))
df_dict[k] = pd.DataFrame(v) data_map[sheet_name].append(list(row.values()))
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count())) logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return df_dict return data_map
class AppAccountHandler(BaseAccountHandler): class AppAccountHandler(BaseAccountHandler):
@ -101,19 +101,19 @@ class AppAccountHandler(BaseAccountHandler):
return filename return filename
@classmethod @classmethod
def create_df(cls): def create_data_map(cls):
df_dict = defaultdict(list) data_map = defaultdict(list)
accounts = Account.get_queryset().select_related('systemuser') accounts = Account.get_queryset().select_related('systemuser')
for account in accounts: for account in accounts:
account.load_auth() account.load_auth()
app_type = account.type app_type = account.type
sheet_name = AppType.get_label(app_type) sheet_name = AppType.get_label(app_type)
row = cls.create_row(account, AppAccountSecretSerializer) row = cls.create_row(account, AppAccountSecretSerializer)
df_dict[sheet_name].append(row) if sheet_name not in data_map:
for k, v in df_dict.items(): data_map[sheet_name].append(list(row.keys()))
df_dict[k] = pd.DataFrame(v) data_map[sheet_name].append(list(row.values()))
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count())) logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
return df_dict return data_map
handler_map = { handler_map = {
@ -142,24 +142,24 @@ class AccountBackupHandler:
if not handler: if not handler:
continue continue
df_dict = handler.create_df() data_map = handler.create_data_map()
if not df_dict: if not data_map:
continue continue
filename = handler.get_filename(self.plan_name) filename = handler.get_filename(self.plan_name)
with pd.ExcelWriter(filename) as w:
for sheet, df in df_dict.items(): wb = Workbook(filename)
sheet = sheet.replace(' ', '-') for sheet, data in data_map.items():
getattr(df, 'to_excel')(w, sheet_name=sheet, index=False) ws = wb.create_sheet(str(sheet))
for row in data:
ws.append(row)
wb.save(filename)
files.append(filename) files.append(filename)
timedelta = round((time.time() - time_start), 2) timedelta = round((time.time() - time_start), 2)
logger.info('步骤完成: 用时 {}s'.format(timedelta)) logger.info('步骤完成: 用时 {}s'.format(timedelta))
return files return files
def send_backup_mail(self, files): def send_backup_mail(self, files, recipients):
recipients = self.execution.plan_snapshot.get('recipients')
if not recipients:
return
if not files: if not files:
return return
recipients = User.objects.filter(id__in=list(recipients)) recipients = User.objects.filter(id__in=list(recipients))
@ -198,8 +198,16 @@ class AccountBackupHandler:
is_success = False is_success = False
error = '-' error = '-'
try: try:
files = self.create_excel() recipients = self.execution.plan_snapshot.get('recipients')
self.send_backup_mail(files) if not recipients:
logger.info(
'\n'
'\033[32m>>> 该备份任务未分配收件人\033[0m'
''
)
else:
files = self.create_excel()
self.send_backup_mail(files, recipients)
except Exception as e: except Exception as e:
self.is_frozen = True self.is_frozen = True
logger.error('任务执行被异常中断') logger.error('任务执行被异常中断')

View File

@ -6,7 +6,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 from orgs.utils import org_aware_func
from ..models import Asset, Connectivity, AuthBook from ..models import Asset, Connectivity, Account
from . import const from . import const
from .utils import clean_ansible_task_hosts, group_asset_by_platform from .utils import clean_ansible_task_hosts, group_asset_by_platform
@ -18,6 +18,7 @@ __all__ = [
] ]
# Todo: 这里可能有问题了
def set_assets_accounts_connectivity(assets, results_summary): def set_assets_accounts_connectivity(assets, results_summary):
asset_ids_ok = set() asset_ids_ok = set()
asset_ids_failed = set() asset_ids_failed = set()
@ -33,11 +34,11 @@ def set_assets_accounts_connectivity(assets, results_summary):
Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok) Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok)
Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed) Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed)
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin') accounts_ok = Account.objects.filter(asset_id__in=asset_ids_ok,)
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin') accounts_failed = Account.objects.filter(asset_id__in=asset_ids_faile)
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok) Account.bulk_set_connectivity(accounts_ok, Connectivity.ok)
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed) Account.bulk_set_connectivity(accounts_failed, Connectivity.failed)
@shared_task(queue="ansible") @shared_task(queue="ansible")

View File

@ -9,6 +9,7 @@ from assets.models import AuthBook
__all__ = ['add_nodes_assets_to_system_users'] __all__ = ['add_nodes_assets_to_system_users']
# Todo: 等待优化
@shared_task @shared_task
@tmp_to_root_org() @tmp_to_root_org()
def add_nodes_assets_to_system_users(nodes_keys, system_users): def add_nodes_assets_to_system_users(nodes_keys, system_users):
@ -17,6 +18,7 @@ def add_nodes_assets_to_system_users(nodes_keys, system_users):
nodes = Node.objects.filter(key__in=nodes_keys) nodes = Node.objects.filter(key__in=nodes_keys)
assets = Node.get_nodes_all_assets(*nodes) assets = Node.get_nodes_all_assets(*nodes)
for system_user in system_users: for system_user in system_users:
""" 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号, """ 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号,
无法更新节点下所有资产的管理用户的问题 """ 无法更新节点下所有资产的管理用户的问题 """
@ -28,7 +30,7 @@ def add_nodes_assets_to_system_users(nodes_keys, system_users):
) )
if created: if created:
need_push_asset_ids.append(asset.id) need_push_asset_ids.append(asset.id)
# # 不再自动更新资产管理用户,只允许用户手动指定。 # 不再自动更新资产管理用户,只允许用户手动指定。
# 只要关联都需要更新资产的管理用户 # 只要关联都需要更新资产的管理用户
# instance.update_asset_admin_user_if_need() # instance.update_asset_admin_user_if_need()

View File

@ -32,17 +32,18 @@ def _dump_args(args: dict):
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty]) return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
def get_push_unixlike_system_user_tasks(system_user, username=None): def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
comment = system_user.name algorithm = kwargs.get('algorithm')
if username is None: if username is None:
username = system_user.username username = system_user.username
comment = system_user.name
if system_user.username_same_with_user: if system_user.username_same_with_user:
from users.models import User from users.models import User
user = User.objects.filter(username=username).only('name', 'username').first() user = User.objects.filter(username=username).only('name', 'username').first()
if user: if user:
comment = f'{system_user.name}[{str(user)}]' comment = f'{system_user.name}[{str(user)}]'
comment = comment.replace(' ', '')
password = system_user.password password = system_user.password
public_key = system_user.public_key public_key = system_user.public_key
@ -104,7 +105,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
'module': 'user', 'module': 'user',
'args': 'name={} shell={} state=present password={}'.format( 'args': 'name={} shell={} state=present password={}'.format(
username, system_user.shell, username, system_user.shell,
encrypt_password(password, salt="K3mIlKK"), encrypt_password(password, salt="K3mIlKK", algorithm=algorithm),
), ),
} }
}) })
@ -138,7 +139,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
return tasks return tasks
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None): def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs):
if username is None: if username is None:
username = system_user.username username = system_user.username
password = system_user.password password = system_user.password
@ -176,7 +177,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
return tasks return tasks
def get_push_system_user_tasks(system_user, platform="unixlike", username=None): def get_push_system_user_tasks(system_user, platform="unixlike", username=None, algorithm=None):
""" """
获取推送系统用户的 ansible 命令跟资产无关 获取推送系统用户的 ansible 命令跟资产无关
:param system_user: :param system_user:
@ -190,16 +191,16 @@ def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
} }
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks) get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
if not system_user.username_same_with_user: if not system_user.username_same_with_user:
return get_tasks(system_user) return get_tasks(system_user, algorithm=algorithm)
tasks = [] tasks = []
# 仅推送这个username # 仅推送这个username
if username is not None: if username is not None:
tasks.extend(get_tasks(system_user, username)) tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
return tasks return tasks
users = system_user.users.all().values_list('username', flat=True) users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users))) print(_("System user is dynamic: {}").format(list(users)))
for _username in users: for _username in users:
tasks.extend(get_tasks(system_user, _username)) tasks.extend(get_tasks(system_user, _username, algorithm=algorithm))
return tasks return tasks
@ -244,7 +245,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
for u in usernames: for u in usernames:
for a in _assets: for a in _assets:
system_user.load_asset_special_auth(a, u) system_user.load_asset_special_auth(a, u)
tasks = get_push_system_user_tasks(system_user, platform, username=u) algorithm = 'des' if a.platform.name == 'AIX' else 'sha512'
tasks = get_push_system_user_tasks(
system_user, platform, username=u,
algorithm=algorithm
)
run_task(tasks, [a]) run_task(tasks, [a])
@ -269,7 +274,7 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
# if username is None: # if username is None:
# username = system_user.username # username = system_user.username
task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format( task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format(
system_user.name, username, asset system_user.name, username or system_user.username, asset
) )
return push_system_user_util(system_user, [asset], task_name=task_name, username=username) return push_system_user_util(system_user, [asset], task_name=task_name, username=username)

View File

@ -8,7 +8,7 @@ from django.utils.translation import ugettext as _, gettext_noop
from assets.models import Asset from assets.models import Asset
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import tmp_to_org, org_aware_func from orgs.utils import tmp_to_org, org_aware_func
from ..models import SystemUser, Connectivity, AuthBook from ..models import SystemUser, Connectivity, Account
from . import const from . import const
from .utils import ( from .utils import (
clean_ansible_task_hosts, group_asset_by_platform clean_ansible_task_hosts, group_asset_by_platform
@ -34,11 +34,11 @@ def set_assets_accounts_connectivity(system_user, assets, results_summary):
else: else:
asset_ids_failed.add(asset.id) asset_ids_failed.add(asset.id)
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser=system_user) accounts_ok = Account.objects.filter(asset_id__in=asset_ids_ok, systemuser=system_user)
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser=system_user) accounts_failed = Account.objects.filter(asset_id__in=asset_ids_failed, systemuser=system_user)
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok) Account.bulk_set_connectivity(accounts_ok, Connectivity.ok)
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed) Account.bulk_set_connectivity(accounts_failed, Connectivity.failed)
@org_aware_func("system_user") @org_aware_func("system_user")

View File

@ -25,7 +25,7 @@ def check_asset_can_run_ansible(asset):
def check_system_user_can_run_ansible(system_user): def check_system_user_can_run_ansible(system_user):
if not system_user.auto_push: if not system_user.auto_push_account:
logger.warn(f'Push system user task skip, auto push not enable: system_user={system_user.name}') logger.warn(f'Push system user task skip, auto push not enable: system_user={system_user.name}')
return False return False
if not system_user.is_protocol_support_push: if not system_user.is_protocol_support_push:

View File

@ -15,6 +15,8 @@ router.register(r'hosts', api.HostViewSet, 'host')
router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'accounts', api.AccountViewSet, 'account') router.register(r'accounts', api.AccountViewSet, 'account')
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history')
router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'system-users', api.SystemUserViewSet, 'system-user') router.register(r'system-users', api.SystemUserViewSet, 'system-user')
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')

View File

@ -3,3 +3,23 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
DEFAULT_CITY = _("Unknown") DEFAULT_CITY = _("Unknown")
MODELS_NEED_RECORD = (
# users
'User', 'UserGroup',
# acls
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
# assets
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
'CommandFilter', 'Platform', 'Account',
# applications
'Application',
# orgs
'Organization',
# settings
'Setting',
# perms
'AssetPermission', 'ApplicationPermission',
# xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2022-05-05 11:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0013_auto_20211130_1037'),
]
operations = [
migrations.AlterField(
model_name='operatelog',
name='action',
field=models.CharField(choices=[('create', 'Create'), ('view', 'View'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action'),
),
]

View File

@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
class OperateLog(OrgModelMixin): class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create' ACTION_CREATE = 'create'
ACTION_VIEW = 'view'
ACTION_UPDATE = 'update' ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete' ACTION_DELETE = 'delete'
ACTION_CHOICES = ( ACTION_CHOICES = (
(ACTION_CREATE, _("Create")), (ACTION_CREATE, _("Create")),
(ACTION_VIEW, _("View")),
(ACTION_UPDATE, _("Update")), (ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete")) (ACTION_DELETE, _("Delete"))
) )

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from django.db.models.signals import ( from django.db.models.signals import (
post_save, m2m_changed, pre_delete post_save, m2m_changed, pre_delete
) )
@ -21,7 +23,7 @@ from jumpserver.utils import current_request
from users.models import User from users.models import User
from users.signals import post_user_change_password from users.signals import post_user_change_password
from terminal.models import Session, Command from terminal.models import Session, Command
from .utils import write_login_log from .utils import write_login_log, create_operate_log
from . import models, serializers from . import models, serializers
from .models import OperateLog from .models import OperateLog
from orgs.utils import current_org from orgs.utils import current_org
@ -36,26 +38,6 @@ logger = get_logger(__name__)
sys_logger = get_syslogger(__name__) sys_logger = get_syslogger(__name__)
json_render = JSONRenderer() json_render = JSONRenderer()
MODELS_NEED_RECORD = (
# users
'User', 'UserGroup',
# acls
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
# assets
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
'CommandFilter', 'Platform', 'AuthBook',
# applications
'Application',
# orgs
'Organization',
# settings
'Setting',
# perms
'AssetPermission', 'ApplicationPermission',
# xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
)
class AuthBackendLabelMapping(LazyObject): class AuthBackendLabelMapping(LazyObject):
@staticmethod @staticmethod
@ -69,7 +51,9 @@ class AuthBackendLabelMapping(LazyObject):
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_DINGTALK] = _('DingTalk') backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
return backend_label_mapping return backend_label_mapping
def _setup(self): def _setup(self):
@ -79,28 +63,6 @@ class AuthBackendLabelMapping(LazyObject):
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping() AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
def create_operate_log(action, sender, resource):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
with translation.override('en'):
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
data = {
"user": str(user), 'action': action, 'resource_type': resource_type,
'resource': str(resource), 'remote_addr': remote_addr,
}
with transaction.atomic():
try:
models.OperateLog.objects.create(**data)
except Exception as e:
logger.error("Create operate log error: {}".format(e))
M2M_NEED_RECORD = { M2M_NEED_RECORD = {
User.groups.through._meta.object_name: ( User.groups.through._meta.object_name: (
_('User and Group'), _('User and Group'),
@ -315,6 +277,7 @@ 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")
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)

View File

@ -7,7 +7,7 @@ from celery import shared_task
from ops.celery.decorator import ( from ops.celery.decorator import (
register_as_period_task register_as_period_task
) )
from .models import UserLoginLog, OperateLog from .models import UserLoginLog, OperateLog, FTPLog
from common.utils import get_log_keep_day from common.utils import get_log_keep_day
@ -29,7 +29,7 @@ def clean_ftp_log_period():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS') days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
OperateLog.objects.filter(datetime__lt=expired_day).delete() FTPLog.objects.filter(datetime__lt=expired_day).delete()
@register_as_period_task(interval=3600*24) @register_as_period_task(interval=3600*24)

View File

@ -1,9 +1,17 @@
import csv import csv
import codecs import codecs
from django.http import HttpResponse
from .const import DEFAULT_CITY from django.http import HttpResponse
from common.utils import validate_ip, get_ip_city from django.db import transaction
from django.utils import translation
from audits.models import OperateLog
from common.utils import validate_ip, get_ip_city, get_request_ip, get_logger
from jumpserver.utils import current_request
from .const import DEFAULT_CITY, MODELS_NEED_RECORD
logger = get_logger(__name__)
def get_excel_response(filename): def get_excel_response(filename):
@ -36,3 +44,25 @@ def write_login_log(*args, **kwargs):
city = get_ip_city(ip) or DEFAULT_CITY city = get_ip_city(ip) or DEFAULT_CITY
kwargs.update({'ip': ip, 'city': city}) kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs) UserLoginLog.objects.create(**kwargs)
def create_operate_log(action, sender, resource):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
with translation.override('en'):
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
data = {
"user": str(user), 'action': action, 'resource_type': resource_type,
'resource': str(resource), 'remote_addr': remote_addr,
}
with transaction.atomic():
try:
OperateLog.objects.create(**data)
except Exception as e:
logger.error("Create operate log error: {}".format(e))

View File

@ -5,9 +5,11 @@ from .connection_token import *
from .token import * from .token import *
from .mfa import * from .mfa import *
from .access_key import * from .access_key import *
from .confirm import *
from .login_confirm import * from .login_confirm import *
from .sso import * from .sso import *
from .wecom import * from .wecom import *
from .dingtalk import * from .dingtalk import *
from .feishu import * from .feishu import *
from .password import * from .password import *
from .temp_token import *

View File

@ -2,14 +2,14 @@
# #
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from common.permissions import IsValidUser
from .. import serializers from .. import serializers
from rbac.permissions import RBACPermission
class AccessKeyViewSet(ModelViewSet): class AccessKeyViewSet(ModelViewSet):
serializer_class = serializers.AccessKeySerializer serializer_class = serializers.AccessKeySerializer
search_fields = ['^id', '^secret'] search_fields = ['^id', '^secret']
permission_classes = [RBACPermission]
def get_queryset(self): def get_queryset(self):
return self.request.user.access_keys.all() return self.request.user.access_keys.all()

View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
#
import time
from django.utils.translation import ugettext_lazy as _
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from rest_framework.response import Response
from rest_framework import status
from common.permissions import IsValidUser, UserConfirmation
from ..const import ConfirmType
from ..serializers import ConfirmSerializer
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
def retrieve(self, request, *args, **kwargs):
return Response('ok')
class ConfirmApi(RetrieveAPIView, CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = ConfirmSerializer
def get_confirm_backend(self, confirm_type):
backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type)
if not backend_classes:
return
for backend_cls in backend_classes:
backend = backend_cls(self.request.user, self.request)
if not backend.check():
continue
return backend
def retrieve(self, request, *args, **kwargs):
confirm_type = request.query_params.get('confirm_type')
backend = self.get_confirm_backend(confirm_type)
if backend is None:
msg = _('This action require verify your MFA')
return Response(data={'error': msg}, status=status.HTTP_404_NOT_FOUND)
data = {
'confirm_type': backend.name,
'content': backend.content,
}
return Response(data=data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
confirm_type = validated_data.get('confirm_type')
mfa_type = validated_data.get('mfa_type')
secret_key = validated_data.get('secret_key')
backend = self.get_confirm_backend(confirm_type)
ok, msg = backend.authenticate(secret_key, mfa_type)
if ok:
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index(confirm_type) + 1
request.session['CONFIRM_TIME'] = int(time.time())
return Response('ok')
return Response({'error': msg}, status=400)

View File

@ -1,73 +1,98 @@
# -*- coding: utf-8 -*-
#
import urllib.parse
import json
from typing import Callable
import os import os
import json
import base64 import base64
import ctypes import urllib.parse
from django.conf import settings
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse
from django.utils import timezone from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from rest_framework.request import Request
from applications.models import Application from common.drf.api import JMSModelViewSet
from authentication.signals import post_auth_failed
from common.utils import get_logger, random_string
from common.mixins.api import SerializerMixin
from common.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true from common.http import is_true
from orgs.mixins.api import RootOrgViewMixin
from perms.models.base import Action from perms.models.base import Action
from perms.utils.application.permission import get_application_actions from terminal.models import EndpointRule
from perms.utils.asset.permission import get_asset_actions
from common.const.http import PATCH
from ..serializers import ( from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer,
ConnectionTokenDisplaySerializer,
) )
from ..models import ConnectionToken
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet']
class ClientProtocolMixin: __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
"""
下载客户端支持的连接文件里面包含了 token 其他连接信息
- [x] RDP
- [ ] KoKo
本质上这里还是暴露出 token 进行使用 class ConnectionTokenMixin:
"""
request: Request request: Request
get_serializer: Callable
create_token: Callable
def get_request_resource(self, serializer): @staticmethod
def check_token_valid(token: ConnectionToken):
is_valid, error = token.check_valid()
if not is_valid:
raise PermissionDenied(error)
@staticmethod
def get_request_resources(serializer):
user = serializer.validated_data.get('user')
asset = serializer.validated_data.get('asset') asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application') application = serializer.validated_data.get('application')
system_user = serializer.validated_data['system_user'] system_user = serializer.validated_data.get('system_user')
return user, asset, application, system_user
user = serializer.validated_data.get('user') @staticmethod
if not user or not self.request.user.is_superuser: def check_user_has_resource_permission(user, asset, application, system_user):
user = self.request.user from perms.utils.asset import has_asset_system_permission
return asset, application, system_user, user from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
target_ip = asset.get_target_ip()
elif application:
target_ip = application.get_target_ip()
else:
target_ip = ''
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
return endpoint
@staticmethod @staticmethod
def parse_env_bool(env_key, env_default, true_value, false_value): def parse_env_bool(env_key, env_default, true_value, false_value):
return true_value if is_true(os.getenv(env_key, env_default)) else false_value return true_value if is_true(os.getenv(env_key, env_default)) else false_value
def get_rdp_file_content(self, serializer): def get_client_protocol_data(self, token: ConnectionToken):
options = { from assets.models import SystemUser
protocol = token.system_user.protocol
username = token.user.username
rdp_config = ssh_token = ''
if protocol == SystemUser.Protocol.rdp:
filename, rdp_config = self.get_rdp_file_info(token)
elif protocol == SystemUser.Protocol.ssh:
filename, ssh_token = self.get_ssh_token(token)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
return {
"filename": filename,
"protocol": protocol,
"username": username,
"token": ssh_token,
"config": rdp_config
}
def get_rdp_file_info(self, token: ConnectionToken):
rdp_options = {
'full address:s': '', 'full address:s': '',
'username:s': '', 'username:s': '',
# 'screen mode id:i': '1', # 'screen mode id:i': '1',
@ -93,419 +118,192 @@ class ClientProtocolMixin:
'bookmarktype:i': '3', 'bookmarktype:i': '3',
'use redirection server name:i': '0', 'use redirection server name:i': '0',
'smart sizing:i': '1', 'smart sizing:i': '1',
#'drivestoredirect:s': '*', # 'drivestoredirect:s': '*',
# 'domain:s': '' # 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench', # 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox', # 'remoteapplicationname:s': 'Firefox',
# 'remoteapplicationcmdline:s': '', # 'remoteapplicationcmdline:s': '',
} }
asset, application, system_user, user = self.get_request_resource(serializer) # 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect:
actions = Action.choices_to_value(token.actions)
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
rdp_options['drivestoredirect:s'] = '*'
# 设置全屏
full_screen = is_true(self.request.query_params.get('full_screen'))
rdp_options['screen mode id:i'] = '2' if full_screen else '1'
# 设置 RDP Server 地址
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=token.asset, application=token.application
)
rdp_options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 设置用户名
rdp_options['username:s'] = '{}|{}'.format(token.user.username, str(token.id))
if token.system_user.ad_domain:
rdp_options['domain:s'] = token.system_user.ad_domain
# 设置宽高
height = self.request.query_params.get('height') height = self.request.query_params.get('height')
width = self.request.query_params.get('width') width = self.request.query_params.get('width')
full_screen = is_true(self.request.query_params.get('full_screen'))
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token, secret = self.create_token(user, asset, application, system_user)
# 设置磁盘挂载
if drives_redirect:
actions = 0
if asset:
actions = get_asset_actions(user, asset, system_user)
elif application:
actions = get_application_actions(user, application, system_user)
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
options['drivestoredirect:s'] = '*'
# 全屏
options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
address = self.request.get_host().split(':')[0] + ':3389'
options['full address:s'] = address
# 用户名
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
options['domain:s'] = system_user.ad_domain
# 宽高
if width and height: if width and height:
options['desktopwidth:i'] = width rdp_options['desktopwidth:i'] = width
options['desktopheight:i'] = height rdp_options['desktopheight:i'] = height
options['winposstr:s:'] = f'0,1,0,0,{width},{height}' rdp_options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') # 设置其他选项
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0') rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
if asset: if token.asset:
name = asset.hostname name = token.asset.hostname
elif application: elif token.application and token.application.category_remote_app:
name = application.name app = '||jmservisor'
application.get_rdp_remote_app_setting() name = token.application.name
rdp_options['remoteapplicationmode:i'] = '1'
app = f'||jmservisor' rdp_options['alternate shell:s'] = app
options['remoteapplicationmode:i'] = '1' rdp_options['remoteapplicationprogram:s'] = app
options['alternate shell:s'] = app rdp_options['remoteapplicationname:s'] = name
options['remoteapplicationprogram:s'] = app
options['remoteapplicationname:s'] = name
options['remoteapplicationcmdline:s'] = '- ' + self.get_encrypt_cmdline(application)
else: else:
name = '*' name = '*'
filename = "{}-{}-jumpserver".format(token.user.username, name)
filename = urllib.parse.quote(filename)
content = '' content = ''
for k, v in options.items(): for k, v in rdp_options.items():
content += f'{k}:{v}\n' content += f'{k}:{v}\n'
return name, content
def get_ssh_token(self, serializer): return filename, content
asset, application, system_user, user = self.get_request_resource(serializer)
token, secret = self.create_token(user, asset, application, system_user) def get_ssh_token(self, token: ConnectionToken):
if asset: if token.asset:
name = asset.hostname name = token.asset.hostname
elif application: elif token.application:
name = application.name name = token.application.name
else: else:
name = '*' name = '*'
filename = f'{token.user.username}-{name}-jumpserver'
content = { endpoint = self.get_smart_endpoint(
'ip': settings.TERMINAL_KOKO_HOST, protocol='ssh', asset=token.asset, application=token.application
'port': str(settings.TERMINAL_KOKO_SSH_PORT),
'username': f'JMS-{token}',
'password': secret
}
token = json.dumps(content)
return name, token
def get_encrypt_cmdline(self, app: Application):
parameters = app.get_rdp_remote_app_setting()['parameters']
parameters = parameters.encode('ascii')
lib_path = get_file_by_arch('xpack/libs', 'librailencrypt.so')
lib = ctypes.CDLL(lib_path)
lib.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_int]
lib.encrypt.restype = ctypes.c_char_p
rst = lib.encrypt(parameters, len(parameters))
rst = rst.decode('ascii')
return rst
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
if self.request.method == 'GET':
data = self.request.query_params
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
name, data = self.get_rdp_file_content(serializer)
response = HttpResponse(data, content_type='application/octet-stream')
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
filename = urllib.parse.quote(filename)
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
def get_valid_serializer(self):
if self.request.method == 'GET':
data = self.request.query_params
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
return serializer
def get_client_protocol_data(self, serializer):
asset, application, system_user, user = self.get_request_resource(serializer)
protocol = system_user.protocol
username = user.username
config, token = '', ''
if protocol == 'rdp':
name, config = self.get_rdp_file_content(serializer)
elif protocol == 'ssh':
name, token = self.get_ssh_token(serializer)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
filename = "{}-{}-jumpserver".format(username, name)
data = {
"filename": filename,
"protocol": system_user.protocol,
"username": username,
"token": token,
"config": config
}
return data
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer()
try:
protocol_data = self.get_client_protocol_data(serializer)
except ValueError as e:
return Response({'error': str(e)}, status=401)
protocol_data = json.dumps(protocol_data).encode()
protocol_data = base64.b64encode(protocol_data).decode()
data = {
'url': 'jms://{}'.format(protocol_data),
}
return Response(data=data)
class SecretDetailMixin:
valid_token: Callable
request: Request
get_serializer: Callable
@staticmethod
def _get_application_secret_detail(application):
gateway = None
remote_app = None
asset = None
if application.category_remote_app:
remote_app = application.get_rdp_remote_app_setting()
asset = application.get_remote_app_asset()
domain = asset.domain
else:
domain = application.domain
if domain and domain.has_gateway():
gateway = domain.random_gateway()
return {
'asset': asset,
'application': application,
'gateway': gateway,
'domain': domain,
'remote_app': remote_app,
}
@staticmethod
def _get_asset_secret_detail(asset):
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
'domain': asset.domain,
'gateway': gateway,
'remote_app': None,
}
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
perm_required = 'authentication.view_connectiontokensecret'
# 非常重要的 api再逻辑层再判断一下双重保险
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token = request.data.get('token', '')
try:
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
except serializers.ValidationError as e:
post_auth_failed.send(
sender=self.__class__, username='', request=self.request,
reason=_('Invalid token')
)
raise e
data = dict(
id=token, secret=value.get('secret', ''),
user=user, system_user=system_user,
expired_at=expired_at, actions=actions
) )
cmd_filter_kwargs = {
'system_user_id': system_user.id,
'user_id': user.id,
}
if asset:
asset_detail = self._get_asset_secret_detail(asset)
system_user.load_asset_more_auth(asset.id, user.username, user.id)
data['type'] = 'asset'
data.update(asset_detail)
cmd_filter_kwargs['asset_id'] = asset.id
else:
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.username, user.id)
data['type'] = 'application'
data.update(app_detail)
cmd_filter_kwargs['application_id'] = app.id
from assets.models import CommandFilterRule
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
data['cmd_filter_rules'] = cmd_filter_rules
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
class TokenCacheMixin:
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
def get_token_cache_key(self, token):
return self.CACHE_KEY_PREFIX.format(token)
def get_token_ttl(self, token):
key = self.get_token_cache_key(token)
return cache.ttl(key)
def set_token_to_cache(self, token, value, ttl=5*60):
key = self.get_token_cache_key(token)
cache.set(key, value, timeout=ttl)
def get_token_from_cache(self, token):
key = self.get_token_cache_key(token)
value = cache.get(key, None)
return value
def renewal_token(self, token, ttl=5*60):
value = self.get_token_from_cache(token)
if value:
pre_ttl = self.get_token_ttl(token)
self.set_token_to_cache(token, value, ttl)
post_ttl = self.get_token_ttl(token)
ok = True
msg = f'{pre_ttl}s is renewed to {post_ttl}s.'
else:
ok = False
msg = 'Token is not found.'
data = { data = {
'ok': ok, 'ip': endpoint.host,
'msg': msg 'port': str(endpoint.ssh_port),
'username': 'JMS-{}'.format(str(token.id)),
'password': token.secret
} }
return data token = json.dumps(data)
return filename, token
class UserConnectionTokenViewSet( class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin, filterset_fields = (
SecretDetailMixin, TokenCacheMixin, GenericViewSet 'type',
): 'user_display', 'system_user_display', 'application_display', 'asset_display'
)
search_fields = filterset_fields
serializer_classes = { serializer_classes = {
'default': ConnectionTokenSerializer, 'default': ConnectionTokenSerializer,
'list': ConnectionTokenDisplaySerializer,
'retrieve': ConnectionTokenDisplaySerializer,
'get_secret_detail': ConnectionTokenSecretSerializer, 'get_secret_detail': ConnectionTokenSecretSerializer,
} }
rbac_perms = { rbac_perms = {
'GET': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken', 'create': 'authentication.add_connectiontoken',
'renewal': 'authentication.add_superconnectiontoken', 'expire': 'authentication.add_connectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret', 'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken', 'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken',
} }
queryset = ConnectionToken.objects.all()
@staticmethod def create_connection_token(self):
def check_resource_permission(user, asset, application, system_user): data = self.request.query_params if self.request.method == 'GET' else self.request.data
from perms.utils.asset import has_asset_system_permission serializer = self.get_serializer(data=data)
from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
return True
@action(methods=[PATCH], detail=False)
def renewal(self, request, *args, **kwargs):
""" 续期 Token """
perm_required = 'authentication.add_superconnectiontoken'
if not request.user.has_perm(perm_required):
raise PermissionDenied('No permissions for authentication.add_superconnectiontoken')
token = request.data.get('token', '')
data = self.renewal_token(token)
status_code = 200 if data.get('ok') else 404
return Response(data=data, status=status_code)
def create_token(self, user, asset, application, system_user, ttl=5*60):
# 再次强调一下权限
perm_required = 'authentication.add_superconnectiontoken'
if user != self.request.user and not self.request.user.has_perm(perm_required):
raise PermissionDenied('Only can create user token')
self.check_resource_permission(user, asset, application, system_user)
token = random_string(36)
secret = random_string(16)
value = {
'id': token,
'secret': secret,
'user': str(user.id),
'username': user.username,
'system_user': str(system_user.id),
'system_user_name': system_user.name,
'created_by': str(self.request.user),
'date_created': str(timezone.now())
}
if asset:
value.update({
'type': 'asset',
'asset': str(asset.id),
'hostname': asset.hostname,
})
elif application:
value.update({
'type': 'application',
'application': application.id,
'application_name': str(application)
})
self.set_token_to_cache(token, value, ttl)
return token, secret
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
token: ConnectionToken = serializer.instance
return token
asset, application, system_user, user = self.get_request_resource(serializer) def perform_create(self, serializer):
token, secret = self.create_token(user, asset, application, system_user) user, asset, application, system_user = self.get_request_resources(serializer)
tp = 'app' if application else 'asset' self.check_user_has_resource_permission(user, asset, application, system_user)
return super(ConnectionTokenViewSet, self).perform_create(serializer)
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
# 非常重要的 api在逻辑层再判断一下双重保险
perm_required = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(perm_required):
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_valid(token)
token.load_system_user_auth()
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')
def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_valid(token)
filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_valid(token)
try:
protocol_data = self.get_client_protocol_data(token)
except ValueError as e:
return Response(data={'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
protocol_data = json.dumps(protocol_data).encode()
protocol_data = base64.b64encode(protocol_data).decode()
data = { data = {
"id": token, 'secret': secret, 'url': 'jms://{}'.format(protocol_data)
'type': tp, 'protocol': system_user.protocol
} }
return Response(data, status=201) return Response(data=data)
def valid_token(self, token): @action(methods=['PATCH'], detail=True)
from users.models import User def expire(self, request, *args, **kwargs):
from assets.models import SystemUser, Asset instance = self.get_object()
from applications.models import Application instance.expire()
from perms.utils.asset.permission import validate_permission as asset_validate_permission return Response(status=status.HTTP_204_NO_CONTENT)
from perms.utils.application.permission import validate_permission as app_validate_permission
value = self.get_token_from_cache(token)
if not value:
raise serializers.ValidationError('Token not found')
user = get_object_or_404(User, id=value.get('user')) class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
if not user.is_valid: serializer_classes = {
raise serializers.ValidationError("User not valid, disabled or expired") 'default': SuperConnectionTokenSerializer,
}
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken'
}
system_user = get_object_or_404(SystemUser, id=value.get('system_user')) @action(methods=['PATCH'], detail=False)
asset = None def renewal(self, request, *args, **kwargs):
app = None from common.utils.timezone import as_current_tz
if value.get('type') == 'asset':
asset = get_object_or_404(Asset, id=value.get('asset'))
if not asset.is_active:
raise serializers.ValidationError("Asset disabled")
has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user)
else:
app = get_object_or_404(Application, id=value.get('application'))
has_perm, actions, expired_at = app_validate_permission(user, app, system_user)
if not has_perm: token_id = request.data.get('token') or ''
raise serializers.ValidationError('Permission expired or invalid') token = get_object_or_404(ConnectionToken, pk=token_id)
return value, user, system_user, asset, app, expired_at, actions date_expired = as_current_tz(token.date_expired)
if token.is_expired:
raise PermissionDenied('Token is expired at: {}'.format(date_expired))
token.renewal()
data = {
'ok': True,
'msg': f'Token is renewed, date expired: {date_expired}'
}
return Response(data=data, status=status.HTTP_200_OK)
def get(self, request):
token = request.query_params.get('token')
value = self.get_token_from_cache(token)
if not value:
return Response('', status=404)
return Response(value)

View File

@ -2,10 +2,11 @@ from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User from users.models import User
from common.utils import get_logger from common.utils import get_logger
from common.permissions import UserConfirmation
from common.mixins.api import RoleUserMixin, RoleAdminMixin from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication.const import ConfirmType
from authentication import errors from authentication import errors
logger = get_logger(__file__) logger = get_logger(__file__)
@ -26,9 +27,8 @@ class DingTalkQRUnBindBase(APIView):
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,) permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
user_id_url_kwarg = 'user_id' user_id_url_kwarg = 'user_id'

View File

@ -2,10 +2,11 @@ from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User from users.models import User
from common.utils import get_logger from common.utils import get_logger
from common.permissions import UserConfirmation
from common.mixins.api import RoleUserMixin, RoleAdminMixin from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication.const import ConfirmType
from authentication import errors from authentication import errors
logger = get_logger(__file__) logger = get_logger(__file__)
@ -26,7 +27,7 @@ class FeiShuQRUnBindBase(APIView):
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase): class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,) permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase): class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):

View File

@ -25,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket() ticket = self.get_ticket()
if ticket: if ticket:
request.session.pop('auth_ticket_id', '') request.session.pop('auth_ticket_id', '')
ticket.close(processor=self.get_user_from_session()) ticket.close()
return Response('', status=200) return Response('', status=200)

View File

@ -10,22 +10,17 @@ from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.response import Response from rest_framework.response import Response
from common.permissions import IsValidUser, NeedMFAVerify
from common.utils import get_logger from common.utils import get_logger
from common.exceptions import UnexpectError from common.exceptions import UnexpectError
from users.models.user import User from users.models.user import User
from ..serializers import OtpVerifySerializer
from .. import serializers from .. import serializers
from .. import errors from .. import errors
from ..mfa.otp import MFAOtp
from ..mixins import AuthMixin from ..mixins import AuthMixin
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
'MFAChallengeVerifyApi', 'UserOtpVerifyApi', 'MFAChallengeVerifyApi', 'MFASendCodeApi'
'MFASendCodeApi'
] ]
@ -88,30 +83,3 @@ class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
raise ValidationError(data) raise ValidationError(data)
except errors.NeedMoreInfoError as e: except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200) return Response(e.as_data(), status=200)
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def get(self, request, *args, **kwargs):
return Response({'code': 'valid', 'msg': 'verified'})
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
otp = MFAOtp(request.user)
ok, error = otp.check_code(code)
if ok:
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": _("Code is invalid, {}").format(error)}, status=400)
def get_permissions(self):
if self.request.method.lower() == 'get' \
and settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [NeedMFAVerify]
return super().get_permissions()

View File

@ -0,0 +1,29 @@
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.decorators import action
from common.drf.api import JMSModelViewSet
from ..models import TempToken
from ..serializers import TempTokenSerializer
from rbac.permissions import RBACPermission
class TempTokenViewSet(JMSModelViewSet):
serializer_class = TempTokenSerializer
permission_classes = [RBACPermission]
http_method_names = ['post', 'get', 'options', 'patch']
rbac_perms = {
'expire': 'authentication.change_temptoken',
}
def get_queryset(self):
username = self.request.user.username
return TempToken.objects.filter(username=username).order_by('-date_created')
@action(methods=['PATCH'], detail=True, url_path='expire')
def expire(self, *args, **kwargs):
instance = self.get_object()
instance.date_expired = timezone.now()
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.shortcuts import redirect
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
@ -28,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
self.create_session_if_need() self.create_session_if_need()
# 如果认证没有过,检查账号密码 # 如果认证没有过,检查账号密码
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try: try:
user = self.check_user_auth_if_need() user = self.get_user_or_auth(serializer.validated_data)
self.check_user_mfa_if_need(user) self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user) self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)

View File

@ -2,10 +2,11 @@ from rest_framework.views import APIView
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User from users.models import User
from common.utils import get_logger from common.utils import get_logger
from common.permissions import UserConfirmation
from common.mixins.api import RoleUserMixin, RoleAdminMixin from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication.const import ConfirmType
from authentication import errors from authentication import errors
logger = get_logger(__file__) logger = get_logger(__file__)
@ -26,9 +27,8 @@ class WeComQRUnBindBase(APIView):
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,) permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
user_id_url_kwarg = 'user_id' user_id_url_kwarg = 'user_id'

View File

@ -1,10 +1,11 @@
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from users.models import User from users.models import User
from common.utils import get_logger from common.utils import get_logger
UserModel = get_user_model()
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -198,6 +198,6 @@ class SignatureAuthentication(signature.SignatureAuthentication):
return None, None return None, None
user, secret = key.user, str(key.secret) user, secret = key.user, str(key.secret)
return user, secret return user, secret
except AccessKey.DoesNotExist: except (AccessKey.DoesNotExist, exceptions.ValidationError):
return None, None return None, None

View File

@ -53,7 +53,7 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
else: else:
built = False built = False
return (user, built) return user, built
def pre_check(self, username, password): def pre_check(self, username, password):
if not settings.AUTH_LDAP: if not settings.AUTH_LDAP:
@ -75,6 +75,9 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
def authenticate(self, request=None, username=None, password=None, **kwargs): def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend') logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password) match, msg = self.pre_check(username, password)
if not match: if not match:
logger.info('Authenticate failed: {}'.format(msg)) logger.info('Authenticate failed: {}'.format(msg))
@ -154,6 +157,8 @@ class LDAPUser(_LDAPUser):
def _populate_user_from_attributes(self): def _populate_user_from_attributes(self):
for field, attr in self.settings.USER_ATTR_MAP.items(): for field, attr in self.settings.USER_ATTR_MAP.items():
if field in ['groups']:
continue
try: try:
value = self.attrs[attr][0] value = self.attrs[attr][0]
value = value.strip() value = value.strip()

View File

@ -18,6 +18,7 @@ from django.urls import reverse
from django.conf import settings from django.conf import settings
from common.utils import get_logger from common.utils import get_logger
from users.utils import construct_user_email
from ..base import JMSBaseAuthBackend from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token, build_absolute_uri from .utils import validate_and_return_id_token, build_absolute_uri
@ -39,17 +40,22 @@ class UserMixin:
logger.debug(log_prompt.format('start')) logger.debug(log_prompt.format('start'))
sub = claims['sub'] sub = claims['sub']
name = claims.get('name', sub)
username = claims.get('preferred_username', sub) # Construct user attrs value
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid')) user_attrs = {}
logger.debug( for field, attr in settings.AUTH_OPENID_USER_ATTR_MAP.items():
log_prompt.format( user_attrs[field] = claims.get(attr, sub)
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email) email = user_attrs.get('email', '')
) email = construct_user_email(user_attrs.get('username'), email)
) user_attrs.update({'email': email})
logger.debug(log_prompt.format(user_attrs))
username = user_attrs.get('username')
name = user_attrs.get('name')
user, created = get_user_model().objects.get_or_create( user, created = get_user_model().objects.get_or_create(
username=username, defaults={"name": name, "email": email} username=username, defaults=user_attrs
) )
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created))) logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => openid create or update user")) logger.debug(log_prompt.format("Send signal => openid create or update user"))
@ -103,21 +109,44 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# Prepares the token payload that will be used to request an authentication token to the # Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider. # token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload')) logger.debug(log_prompt.format('Prepares token payload'))
"""
The reason for need not client_id and client_secret in token_payload.
OIDC protocol indicate client's token_endpoint_auth_method only accept one type in
- client_secret_basic
- client_secret_post
- client_secret_jwt
- private_key_jwt
- none
If the client offer more than one auth method type to OIDC, OIDC will auth client failed.
OIDC default use client_secret_basic,
this type only need in headers add Authorization=Basic xxx.
More info see: https://github.com/jumpserver/jumpserver/issues/8165
More info see: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
"""
token_payload = { token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': build_absolute_uri( 'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME) request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
) )
} }
if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post':
# Prepares the token headers that will be used to request an authentication token to the token_payload.update({
# token endpoint of the OIDC provider. 'client_id': settings.AUTH_OPENID_CLIENT_ID,
logger.debug(log_prompt.format('Prepares token headers')) 'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET) })
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())} headers = None
else:
# Prepares the token headers that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token headers'))
basic_token = "{}:{}".format(
settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET
)
headers = {
"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())
}
# Calls the token endpoint. # Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint')) logger.debug(log_prompt.format('Call the token endpoint'))
@ -258,6 +287,11 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
try: try:
claims_response.raise_for_status() claims_response.raise_for_status()
claims = claims_response.json() claims = claims_response.json()
preferred_username = claims.get('preferred_username')
if preferred_username and \
preferred_username.lower() == username.lower() and \
preferred_username != username:
return
except Exception as e: except Exception as e:
error = "Json claims response error, claims response " \ error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e)) "content is: {}, error is: {}".format(claims_response.content, str(e))
@ -286,5 +320,3 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
openid_user_login_failed.send( openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid" sender=self.__class__, request=request, username=username, reason="User is invalid"
) )
return None

View File

@ -13,20 +13,23 @@ User = get_user_model()
class CreateUserMixin: class CreateUserMixin:
def get_django_user(self, username, password=None, *args, **kwargs): @staticmethod
def get_django_user(username, password=None, *args, **kwargs):
if isinstance(username, bytes): if isinstance(username, bytes):
username = username.decode() username = username.decode()
try: user = User.objects.filter(username=username).first()
user = User.objects.get(username=username) if user:
except User.DoesNotExist: return user
if '@' in username:
email = username if '@' in username:
else: email = username
email_suffix = settings.EMAIL_SUFFIX else:
email = '{}@{}'.format(username, email_suffix) email_suffix = settings.EMAIL_SUFFIX
user = User(username=username, name=username, email=email) email = '{}@{}'.format(username, email_suffix)
user.source = user.Source.radius.value
user.save() user = User(username=username, name=username, email=email)
user.source = user.Source.radius.value
user.save()
return user return user
def _perform_radius_auth(self, client, packet): def _perform_radius_auth(self, client, packet):

View File

@ -14,7 +14,7 @@ from ..base import JMSModelBackend
__all__ = ['SAML2Backend'] __all__ = ['SAML2Backend']
logger = get_logger(__file__) logger = get_logger(__name__)
class SAML2Backend(JMSModelBackend): class SAML2Backend(JMSModelBackend):

View File

@ -74,27 +74,37 @@ class PrepareRequestMixin:
return idp_settings return idp_settings
@staticmethod @staticmethod
def get_attribute_consuming_service(): def get_request_attributes():
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES attr_mapping = settings.SAML2_RENAME_ATTRIBUTES or {}
if attr_mapping and isinstance(attr_mapping, dict): attr_map_reverse = {v: k for k, v in attr_mapping.items()}
attr_list = [ need_attrs = (
{ ('username', 'username', True),
"name": sp_key, ('email', 'email', True),
"friendlyName": idp_key, "isRequired": True ('name', 'name', False),
} ('phone', 'phone', False),
for idp_key, sp_key in attr_mapping.items() ('comment', 'comment', False),
] )
request_attribute_template = { attr_list = []
"attributeConsumingService": { for name, friend_name, is_required in need_attrs:
"isDefault": False, rename_name = attr_map_reverse.get(friend_name)
"serviceName": "JumpServer", name = rename_name if rename_name else name
"serviceDescription": "JumpServer", attr_list.append({
"requestedAttributes": attr_list "name": name, "isRequired": is_required,
} "friendlyName": friend_name,
})
return attr_list
def get_attribute_consuming_service(self):
attr_list = self.get_request_attributes()
request_attribute_template = {
"attributeConsumingService": {
"isDefault": False,
"serviceName": "JumpServer",
"serviceDescription": "JumpServer",
"requestedAttributes": attr_list
} }
return request_attribute_template }
else: return request_attribute_template
return {}
@staticmethod @staticmethod
def get_advanced_settings(): def get_advanced_settings():
@ -167,11 +177,14 @@ class PrepareRequestMixin:
def get_attributes(self, saml_instance): def get_attributes(self, saml_instance):
user_attrs = {} user_attrs = {}
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
attrs = saml_instance.get_attributes() attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone'] valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
for attr, value in attrs.items(): for attr, value in attrs.items():
attr = attr.rsplit('/', 1)[-1] attr = attr.rsplit('/', 1)[-1]
if attr_mapping and attr_mapping.get(attr):
attr = attr_mapping.get(attr)
if attr not in valid_attrs: if attr not in valid_attrs:
continue continue
user_attrs[attr] = self.value_to_str(value) user_attrs[attr] = self.value_to_str(value)

View File

@ -0,0 +1,26 @@
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import PermissionDenied
from authentication.models import TempToken
from .base import JMSModelBackend
class TempTokenAuthBackend(JMSModelBackend):
model = TempToken
def authenticate(self, request, username='', password='', *args, **kwargs):
token = self.model.objects.filter(username=username, secret=password).first()
if not token:
return None
if not token.is_valid:
raise PermissionDenied('Token is invalid, expired at {}'.format(token.date_expired))
token.verified = True
token.date_verified = timezone.now()
token.save()
return token.user
@staticmethod
def is_enabled():
return settings.AUTH_TEMP_TOKEN

View File

@ -0,0 +1,5 @@
from .mfa import ConfirmMFA
from .password import ConfirmPassword
from .relogin import ConfirmReLogin
CONFIRM_BACKENDS = [ConfirmReLogin, ConfirmPassword, ConfirmMFA]

View File

@ -0,0 +1,30 @@
import abc
class BaseConfirm(abc.ABC):
def __init__(self, user, request):
self.user = user
self.request = request
@property
@abc.abstractmethod
def name(self) -> str:
return ''
@property
@abc.abstractmethod
def display_name(self) -> str:
return ''
@abc.abstractmethod
def check(self) -> bool:
return False
@property
def content(self):
return ''
@abc.abstractmethod
def authenticate(self, secret_key, mfa_type) -> tuple:
return False, 'Error msg'

View File

@ -0,0 +1,26 @@
from users.models import User
from .base import BaseConfirm
class ConfirmMFA(BaseConfirm):
name = 'mfa'
display_name = 'MFA'
def check(self):
return self.user.active_mfa_backends and self.user.mfa_enabled
@property
def content(self):
backends = User.get_user_mfa_backends(self.user)
return [{
'name': backend.name,
'disabled': not bool(backend.is_active()),
'display_name': backend.display_name,
'placeholder': backend.placeholder,
} for backend in backends]
def authenticate(self, secret_key, mfa_type):
mfa_backend = self.user.get_mfa_backend_by_type(mfa_type)
ok, msg = mfa_backend.check_code(secret_key)
return ok, msg

View File

@ -0,0 +1,17 @@
from django.utils.translation import ugettext_lazy as _
from authentication.mixins import authenticate
from .base import BaseConfirm
class ConfirmPassword(BaseConfirm):
name = 'password'
display_name = _('Password')
def check(self):
return self.user.is_password_authenticate()
def authenticate(self, secret_key, mfa_type):
ok = authenticate(self.request, username=self.user.username, password=secret_key)
msg = '' if ok else _('Authentication failed password incorrect')
return ok, msg

View File

@ -0,0 +1,30 @@
from datetime import datetime
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from .base import BaseConfirm
SPECIFIED_TIME = 5
RELOGIN_ERROR = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
class ConfirmReLogin(BaseConfirm):
name = 'relogin'
display_name = 'Re-Login'
def check(self):
return not self.user.is_password_authenticate()
def authenticate(self, secret_key, mfa_type):
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
login_time = self.request.session.get('login_time')
msg = RELOGIN_ERROR
if not login_time:
return False, msg
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
return False, msg
return True, ''

View File

@ -1,2 +1,37 @@
from django.db.models import TextChoices
from authentication.confirm import CONFIRM_BACKENDS
from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin
from .mfa import MFAOtp, MFASms, MFARadius
RSA_PRIVATE_KEY = 'rsa_private_key' RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key' RSA_PUBLIC_KEY = 'rsa_public_key'
CONFIRM_BACKEND_MAP = {backend.name: backend for backend in CONFIRM_BACKENDS}
class ConfirmType(TextChoices):
ReLogin = ConfirmReLogin.name, ConfirmReLogin.display_name
PASSWORD = ConfirmPassword.name, ConfirmPassword.display_name
MFA = ConfirmMFA.name, ConfirmMFA.display_name
@classmethod
def get_can_confirm_types(cls, confirm_type):
start = cls.values.index(confirm_type)
types = cls.values[start:]
types.reverse()
return types
@classmethod
def get_can_confirm_backend_classes(cls, confirm_type):
types = cls.get_can_confirm_types(confirm_type)
backend_classes = [
CONFIRM_BACKEND_MAP[tp] for tp in types if tp in CONFIRM_BACKEND_MAP
]
return backend_classes
class MFAType(TextChoices):
OTP = MFAOtp.name, MFAOtp.display_name
SMS = MFASms.name, MFASms.display_name
Radius = MFARadius.name, MFARadius.display_name

View File

@ -1,367 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.conf import settings
from rest_framework import status
from common.exceptions import JMSException
from .signals import post_auth_failed
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
reason_user_inactive = 'user_inactive'
reason_user_expired = 'user_expired'
reason_backend_not_match = 'backend_not_match'
reason_acl_not_allow = 'acl_not_allow'
only_local_users_are_allowed = 'only_local_users_are_allowed'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_password_decrypt_failed: _('Password decrypt failed'),
reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'),
reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive."),
reason_user_expired: _("This account is expired"),
reason_backend_not_match: _("Auth backend not match"),
reason_acl_not_allow: _("ACL is not allowed"),
only_local_users_are_allowed: _("Only local users are allowed")
}
old_reason_choices = {
'0': '-',
'1': reason_choices[reason_password_failed],
'2': reason_choices[reason_mfa_failed],
'3': reason_choices[reason_user_not_exist],
'4': reason_choices[reason_password_expired],
}
session_empty_msg = _("No session found, check your cookie")
invalid_login_msg = _(
"The username or password you entered is incorrect, "
"please enter it again. "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
block_user_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_ip_login_msg = _(
"The ip has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_error_msg = _(
"{error}, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_required_msg = _("MFA required")
mfa_unset_msg = _("MFA not set, please set it first")
login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}")
class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.error
)
class AuthFailedNeedBlockMixin:
username = ''
ip = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
LoginBlockUtil(self.username, self.ip).incr_failed_count()
class AuthFailedError(Exception):
username = ''
msg = ''
error = ''
request = None
ip = ''
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
def __str__(self):
return str(self.msg)
class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
def __init__(self, username, ip, **kwargs):
self.msg = block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
LoginIpBlockUtil(ip).set_block_if_need()
super().__init__(username=username, ip=ip, **kwargs)
class CredentialError(
AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, BlockGlobalIpLoginError, AuthFailedError
):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
util = LoginBlockUtil(username, ip)
times_remainder = util.get_remainder_times()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder < 1:
self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
return
default_msg = invalid_login_msg.format(
times_try=times_remainder, block_time=block_time
)
if error == reason_password_failed:
self.msg = default_msg
else:
self.msg = reason_choices.get(error, default_msg)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_failed
msg: str
def __init__(self, username, request, ip, mfa_type, error):
super().__init__(username=username, request=request)
util = MFABlockUtils(username, ip)
times_remainder = util.incr_failed_count()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder:
self.msg = mfa_error_msg.format(
error=error, times_try=times_remainder, block_time=block_time
)
else:
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
error = 'block_mfa'
def __init__(self, username, request, ip):
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request, ip=ip)
class MFAUnsetError(Exception):
error = reason_mfa_unset
msg = mfa_unset_msg
def __init__(self, user, request, url):
self.url = url
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
def __init__(self, username, ip):
self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, ip=ip)
class SessionEmptyError(AuthFailedError):
msg = session_empty_msg
error = 'session_empty'
class NeedMoreInfoError(Exception):
error = ''
msg = ''
def __init__(self, error='', msg=''):
if error:
self.error = error
if msg:
self.msg = msg
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
class MFARequiredError(NeedMoreInfoError):
msg = mfa_required_msg
error = 'mfa_required'
def __init__(self, error='', msg='', mfa_types=()):
super().__init__(error=error, msg=msg)
self.choices = mfa_types
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
'data': {
'choices': self.choices,
'url': reverse('api-auth:mfa-challenge')
}
}
class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
msg = reason_acl_not_allow
error = 'acl_error'
def __init__(self, msg, **kwargs):
self.msg = msg
super().__init__(**kwargs)
def as_data(self):
return {
"error": reason_acl_not_allow,
"msg": self.msg
}
class LoginIPNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("IP is not allowed"), **kwargs)
class TimePeriodNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("Time Period is not allowed"), **kwargs)
class LoginConfirmBaseError(NeedMoreInfoError):
def __init__(self, ticket_id, **kwargs):
self.ticket_id = ticket_id
super().__init__(**kwargs)
def as_data(self):
return {
"error": self.error,
"msg": self.msg,
"data": {
"ticket_id": self.ticket_id
}
}
class LoginConfirmWaitError(LoginConfirmBaseError):
msg = login_confirm_wait_msg
error = 'login_confirm_wait'
class LoginConfirmOtherError(LoginConfirmBaseError):
error = 'login_confirm_error'
def __init__(self, ticket_id, status):
msg = login_confirm_error_msg.format(status)
super().__init__(ticket_id=ticket_id, msg=msg)
class SSOAuthClosed(JMSException):
default_code = 'sso_auth_closed'
default_detail = _('SSO auth closed')
class PasswordTooSimple(JMSException):
default_code = 'passwd_too_simple'
default_detail = _('Your password is too simple, please change it for security')
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
class PasswordNeedUpdate(JMSException):
default_code = 'passwd_need_update'
default_detail = _('You should to change your password before login')
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
class PasswordRequireResetError(JMSException):
default_code = 'passwd_has_expired'
default_detail = _('Your password has expired, please reset before logging in')
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
class WeComCodeInvalid(JMSException):
default_code = 'wecom_code_invalid'
default_detail = 'Code invalid, can not get user info'
class WeComBindAlready(JMSException):
default_code = 'wecom_bind_already'
default_detail = 'WeCom already binded'
class WeComNotBound(JMSException):
default_code = 'wecom_not_bound'
default_detail = 'WeCom is not bound'
class DingTalkNotBound(JMSException):
default_code = 'dingtalk_not_bound'
default_detail = 'DingTalk is not bound'
class FeiShuNotBound(JMSException):
default_code = 'feishu_not_bound'
default_detail = 'FeiShu is not bound'
class PasswordInvalid(JMSException):
default_code = 'passwd_invalid'
default_detail = _('Your password is invalid')
class MFACodeRequiredError(AuthFailedError):
error = 'mfa_code_required'
msg = _("Please enter MFA code")
class SMSCodeRequiredError(AuthFailedError):
error = 'sms_code_required'
msg = _("Please enter SMS code")
class UserPhoneNotSet(AuthFailedError):
error = 'phone_not_set'
msg = _('Phone not set')

View File

@ -0,0 +1,4 @@
from .const import *
from .mfa import *
from .failed import *
from .redirect import *

View File

@ -0,0 +1,67 @@
from django.utils.translation import gettext_lazy as _
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
reason_user_inactive = 'user_inactive'
reason_user_expired = 'user_expired'
reason_backend_not_match = 'backend_not_match'
reason_acl_not_allow = 'acl_not_allow'
only_local_users_are_allowed = 'only_local_users_are_allowed'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_password_decrypt_failed: _('Password decrypt failed'),
reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'),
reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive."),
reason_user_expired: _("This account is expired"),
reason_backend_not_match: _("Auth backend not match"),
reason_acl_not_allow: _("ACL is not allowed"),
only_local_users_are_allowed: _("Only local users are allowed")
}
old_reason_choices = {
'0': '-',
'1': reason_choices[reason_password_failed],
'2': reason_choices[reason_mfa_failed],
'3': reason_choices[reason_user_not_exist],
'4': reason_choices[reason_password_expired],
}
session_empty_msg = _("No session found, check your cookie")
invalid_login_msg = _(
"The username or password you entered is incorrect, "
"please enter it again. "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
block_user_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_ip_login_msg = _(
"The ip has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_error_msg = _(
"{error}, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_required_msg = _("MFA required")
mfa_unset_msg = _("MFA not set, please set it first")
login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}")

View File

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from ..signals import post_auth_failed
from . import const
class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.error
)
class AuthFailedNeedBlockMixin:
username = ''
ip = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
LoginBlockUtil(self.username, self.ip).incr_failed_count()
class AuthFailedError(Exception):
username = ''
msg = ''
error = ''
request = None
ip = ''
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
def __str__(self):
return str(self.msg)
class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
def __init__(self, username, ip, **kwargs):
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
LoginIpBlockUtil(ip).set_block_if_need()
super().__init__(username=username, ip=ip, **kwargs)
class CredentialError(
AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin,
BlockGlobalIpLoginError, AuthFailedError
):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
util = LoginBlockUtil(username, ip)
times_remainder = util.get_remainder_times()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder < 1:
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
return
default_msg = const.invalid_login_msg.format(
times_try=times_remainder, block_time=block_time
)
if error == const.reason_password_failed:
self.msg = default_msg
else:
self.msg = const.reason_choices.get(error, default_msg)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = const.reason_mfa_failed
msg: str
def __init__(self, username, request, ip, mfa_type, error):
super().__init__(username=username, request=request)
util = MFABlockUtils(username, ip)
times_remainder = util.incr_failed_count()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder:
self.msg = const.mfa_error_msg.format(
error=error, times_try=times_remainder, block_time=block_time
)
else:
self.msg = const.block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
error = 'block_mfa'
def __init__(self, username, request, ip):
self.msg = const.block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request, ip=ip)
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
def __init__(self, username, ip):
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, ip=ip)
class SessionEmptyError(AuthFailedError):
msg = const.session_empty_msg
error = 'session_empty'
class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
msg = const.reason_acl_not_allow
error = 'acl_error'
def __init__(self, msg, **kwargs):
self.msg = msg
super().__init__(**kwargs)
def as_data(self):
return {
"error": const.reason_acl_not_allow,
"msg": self.msg
}
class LoginIPNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("IP is not allowed"), **kwargs)
class TimePeriodNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("Time Period is not allowed"), **kwargs)
class MFACodeRequiredError(AuthFailedError):
error = 'mfa_code_required'
msg = _("Please enter MFA code")
class SMSCodeRequiredError(AuthFailedError):
error = 'sms_code_required'
msg = _("Please enter SMS code")
class UserPhoneNotSet(AuthFailedError):
error = 'phone_not_set'
msg = _('Phone not set')

View File

@ -0,0 +1,38 @@
from django.utils.translation import ugettext_lazy as _
from common.exceptions import JMSException
class SSOAuthClosed(JMSException):
default_code = 'sso_auth_closed'
default_detail = _('SSO auth closed')
class WeComCodeInvalid(JMSException):
default_code = 'wecom_code_invalid'
default_detail = 'Code invalid, can not get user info'
class WeComBindAlready(JMSException):
default_code = 'wecom_bind_already'
default_detail = 'WeCom already binded'
class WeComNotBound(JMSException):
default_code = 'wecom_not_bound'
default_detail = 'WeCom is not bound'
class DingTalkNotBound(JMSException):
default_code = 'dingtalk_not_bound'
default_detail = 'DingTalk is not bound'
class FeiShuNotBound(JMSException):
default_code = 'feishu_not_bound'
default_detail = 'FeiShu is not bound'
class PasswordInvalid(JMSException):
default_code = 'passwd_invalid'
default_detail = _('Your password is invalid')

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