mirror of https://github.com/jumpserver/jumpserver
perf: merge with dev
commit
93a89509d6
|
@ -1,2 +1,3 @@
|
|||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
|
@ -34,10 +34,11 @@ celerybeat-schedule.db
|
|||
data/static
|
||||
docs/_build/
|
||||
xpack
|
||||
xpack.bak
|
||||
logs/*
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
release/*
|
||||
releashe
|
||||
/apps/script.py
|
||||
xpack.bak
|
||||
data/*
|
||||
|
|
59
Dockerfile
59
Dockerfile
|
@ -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
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
|
@ -44,11 +29,12 @@ ARG TOOLS=" \
|
|||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
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 \
|
||||
&& apt update \
|
||||
&& apt update && sleep 1 && apt update \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& 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 \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
|
||||
RUN mkdir -p /opt/jumpserver/oracle/ \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
|
||||
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
ARG TARGETARCH
|
||||
ARG ORACLE_LIB_MAJOR=19
|
||||
ARG ORACLE_LIB_MINOR=10
|
||||
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
|
||||
|
||||
RUN mkdir -p /opt/oracle/ \
|
||||
&& 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 \
|
||||
&& 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 \
|
||||
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
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 -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& 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/logs
|
||||
|
||||
|
|
61
README.md
61
README.md
|
@ -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>
|
||||
|
||||
<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://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://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>
|
||||
</p>
|
||||
|
||||
|
@ -15,7 +18,7 @@
|
|||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
|
@ -28,9 +31,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
|||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多租户: 一套系统,多个子公司或部门同时使用;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
### UI 展示
|
||||
|
@ -55,12 +58,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
|||
- [手动安装](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)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目
|
||||
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目
|
||||
| 项目 | 状态 | 描述 |
|
||||
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
|
||||
| [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 项目 |
|
||||
| [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 项目 |
|
||||
| [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) |
|
||||
| [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 更加完善
|
||||
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
|
||||
<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>
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
|
||||
### 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web 页面连接使用数据库,JumpServer Web 数据库依赖
|
||||
|
||||
|
||||
### JumpServer 企业版
|
||||
|
@ -103,14 +95,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
|||
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851);
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516);
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732);
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708);
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信: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
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||
asset=self.serializer.asset,
|
||||
system_user=self.serializer.system_user,
|
||||
assignees=acl.reviewers.all(),
|
||||
org_id=self.serializer.org.id
|
||||
org_id=self.serializer.org.id,
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-tickets:super-ticket-status',
|
||||
|
@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||
external=True, api_to_ui=True
|
||||
)
|
||||
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 = {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
|
|
|
@ -97,32 +97,25 @@ class LoginACL(BaseACL):
|
|||
|
||||
return allow, reject_type
|
||||
|
||||
@staticmethod
|
||||
def construct_confirm_ticket_meta(request=None):
|
||||
def create_confirm_ticket(self, request):
|
||||
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 = login_ip or '0.0.0.0'
|
||||
login_city = get_ip_city(login_ip)
|
||||
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 = {
|
||||
'title': ticket_title,
|
||||
'type': const.TicketType.login_confirm.value,
|
||||
'meta': ticket_meta,
|
||||
'title': title,
|
||||
'type': const.TicketType.login_confirm,
|
||||
'applicant': self.user,
|
||||
'apply_login_city': login_city,
|
||||
'apply_login_ip': login_ip,
|
||||
'apply_login_datetime': login_datetime,
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(self.user)
|
||||
ticket = ApplyLoginTicket.objects.create(**data)
|
||||
assignees = self.reviewers.all()
|
||||
ticket.open_by_system(assignees)
|
||||
return ticket
|
||||
|
|
|
@ -85,19 +85,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
|||
@classmethod
|
||||
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
|
||||
from tickets.const import TicketType
|
||||
from tickets.models import Ticket
|
||||
from tickets.models import ApplyLoginAssetTicket
|
||||
title = _('Login asset confirm') + ' ({})'.format(user)
|
||||
data = {
|
||||
'title': _('Login asset confirm') + ' ({})'.format(user),
|
||||
'title': title,
|
||||
'type': TicketType.login_asset_confirm,
|
||||
'meta': {
|
||||
'apply_login_user': str(user),
|
||||
'apply_login_asset': str(asset),
|
||||
'apply_login_system_user': str(system_user),
|
||||
},
|
||||
'applicant': user,
|
||||
'apply_login_user': user,
|
||||
'apply_login_asset': asset,
|
||||
'apply_login_system_user': system_user,
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(assignees)
|
||||
ticket.open(applicant=user)
|
||||
ticket = ApplyLoginAssetTicket.objects.create(**data)
|
||||
ticket.open_by_system(assignees)
|
||||
return ticket
|
||||
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
#
|
||||
|
||||
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.api import JMSBulkModelViewSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from common.permissions import UserConfirmation
|
||||
from authentication.const import ConfirmType
|
||||
from rbac.permissions import RBACPermission
|
||||
from assets.models import SystemUser
|
||||
from ..models import Account
|
||||
from ..hands import NeedMFAVerify
|
||||
from .. import serializers
|
||||
|
||||
|
||||
|
@ -54,9 +56,9 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
|||
perm_model = SystemUser
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||
http_method_names = ['get', 'options']
|
||||
rbac_perms = {
|
||||
'retrieve': 'applications.view_applicationaccountsecret',
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
from django.shortcuts import get_object_or_404
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
|
|
@ -11,5 +11,4 @@
|
|||
"""
|
||||
|
||||
|
||||
from common.permissions import NeedMFAVerify
|
||||
from users.models import User, UserGroup
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
|||
('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')),
|
||||
('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')),
|
||||
('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')),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 3.1.12 on 2021-08-26 09:07
|
||||
|
||||
import assets.models.base
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
@ -26,9 +26,9 @@ class Migration(migrations.Migration):
|
|||
('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, 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')),
|
||||
('private_key', common.fields.model.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')),
|
||||
('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')),
|
||||
|
@ -56,9 +56,9 @@ class Migration(migrations.Migration):
|
|||
('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, 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')),
|
||||
('private_key', common.fields.model.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')),
|
||||
('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')),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -44,6 +44,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
|||
def category_remote_app(self):
|
||||
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):
|
||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||
if not self.category_remote_app:
|
||||
|
@ -76,3 +84,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
|||
if raise_exception:
|
||||
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
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.serializers.base import AuthSerializerMixin
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from common.drf.serializers import MethodSerializer, SecretReadableMixin
|
||||
from .attrs import (
|
||||
category_serializer_classes_mapping,
|
||||
type_serializer_classes_mapping,
|
||||
|
@ -152,7 +152,7 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
|
|||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AppAccountSecretSerializer(AppAccountSerializer):
|
||||
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
class Meta(AppAccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
|
||||
|
@ -13,19 +14,21 @@ class ChromeSerializer(RemoteAppSerializer):
|
|||
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
|
||||
)
|
||||
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(
|
||||
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(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class ChromeSecretSerializer(ChromeSerializer):
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True, write_only=False
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
|
||||
|
@ -19,14 +20,14 @@ class CustomSerializer(RemoteAppSerializer):
|
|||
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Custom password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class CustomSecretSerializer(RemoteAppSerializer):
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Custom password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
|
||||
|
@ -29,14 +30,14 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
|||
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Mysql workbench password'),
|
||||
allow_null=True,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Mysql workbench password'),
|
||||
allow_null=True,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
|
||||
|
@ -25,14 +26,14 @@ class VMwareClientSerializer(RemoteAppSerializer):
|
|||
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Vmware password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class VMwareClientSecretSerializer(RemoteAppSerializer):
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Vmware password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
|
|
@ -12,3 +12,4 @@ from .cmd_filter import *
|
|||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .account_backup import *
|
||||
from .account_history import *
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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_filters import rest_framework as filters
|
||||
from rest_framework.decorators import action
|
||||
|
@ -8,12 +8,14 @@ from rest_framework.generics import CreateAPIView
|
|||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
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 ..models import AuthBook, Node
|
||||
from ..models import Node, Account
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
|
||||
__all__ = ['AccountFilterSet', 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
|
||||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
|
@ -48,16 +50,16 @@ class AccountFilterSet(BaseFilterSet):
|
|||
return qs
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
model = Account
|
||||
fields = [
|
||||
'asset', 'systemuser', 'id',
|
||||
'asset', 'id',
|
||||
]
|
||||
|
||||
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = AuthBook
|
||||
filterset_fields = ("username", "asset", "systemuser", 'ip', 'hostname')
|
||||
search_fields = ('username', 'ip', 'hostname', 'systemuser__username')
|
||||
model = Account
|
||||
filterset_fields = ("username", "asset", 'ip', 'hostname')
|
||||
search_fields = ('username', 'ip', 'hostname')
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
|
@ -68,10 +70,6 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||
'partial_update': 'assets.change_assetaccountsecret',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = AuthBook.get_queryset()
|
||||
return queryset
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='verify')
|
||||
def verify_account(self, request, *args, **kwargs):
|
||||
account = super().get_object()
|
||||
|
@ -79,7 +77,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||
return Response(data={'task': task.id})
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountViewSet):
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
|
@ -87,7 +85,7 @@ class AccountSecretsViewSet(AccountViewSet):
|
|||
'default': serializers.AccountSecretSerializer
|
||||
}
|
||||
http_method_names = ['get']
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_assetaccountsecret',
|
||||
'retrieve': 'assets.view_assetaccountsecret',
|
||||
|
@ -104,7 +102,7 @@ class AccountTaskCreateAPI(CreateAPIView):
|
|||
return request.user.has_perm('assets.test_assetconnectivity')
|
||||
|
||||
def get_accounts(self):
|
||||
queryset = AuthBook.objects.all()
|
||||
queryset = Account.objects.all()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
|
|||
external=True, api_to_ui=True
|
||||
)
|
||||
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 {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
|
|
|
@ -4,7 +4,6 @@ from rest_framework.response import Response
|
|||
from rest_framework.decorators import action
|
||||
|
||||
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.mixins.api import SuggestionMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
@ -103,27 +102,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
|||
permission_classes = (IsValidUser,)
|
||||
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):
|
||||
serializer = super().get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
pk = kwargs.get('pk')
|
||||
data = self.decrypt_data_if_need(serializer.validated_data)
|
||||
instance_id = data.get('instance_id')
|
||||
data = serializer.validated_data
|
||||
asset_or_app_id = data.get('instance_id')
|
||||
|
||||
with tmp_to_root_org():
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@ class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
|
|||
|
||||
|
||||
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
perm_model = models.AuthBook
|
||||
serializer_class = serializers.SystemUserAssetRelationSerializer
|
||||
model = models.SystemUser.assets.through
|
||||
filterset_fields = [
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 2.1.7 on 2019-06-24 13:08
|
||||
|
||||
import assets.models.utils
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
@ -15,61 +15,61 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
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(
|
||||
model_name='adminuser',
|
||||
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(
|
||||
model_name='adminuser',
|
||||
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(
|
||||
model_name='authbook',
|
||||
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(
|
||||
model_name='authbook',
|
||||
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(
|
||||
model_name='authbook',
|
||||
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(
|
||||
model_name='gateway',
|
||||
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(
|
||||
model_name='gateway',
|
||||
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(
|
||||
model_name='gateway',
|
||||
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(
|
||||
model_name='systemuser',
|
||||
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(
|
||||
model_name='systemuser',
|
||||
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(
|
||||
model_name='systemuser',
|
||||
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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
|
||||
|
||||
|
@ -14,21 +14,21 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
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(
|
||||
model_name='authbook',
|
||||
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(
|
||||
model_name='gateway',
|
||||
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(
|
||||
model_name='systemuser',
|
||||
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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@ class Migration(migrations.Migration):
|
|||
('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')),
|
||||
('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')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
@ -58,9 +58,9 @@ class Migration(migrations.Migration):
|
|||
('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, 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')),
|
||||
('private_key', common.fields.model.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')),
|
||||
('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')),
|
||||
|
|
|
@ -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)
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -14,3 +14,4 @@ from .authbook import *
|
|||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .backup import *
|
||||
from .account import *
|
||||
|
|
|
@ -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)
|
|
@ -11,6 +11,7 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from common.db.fields import JsonDictTextField
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from assets.const import Category, AllTypes
|
||||
|
@ -147,7 +148,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
|||
|
||||
# Some information
|
||||
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"))
|
||||
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):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
|
||||
def set_admin_user_relation(self):
|
||||
from assets.models import AuthBook
|
||||
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)
|
||||
def get_target_ip(self):
|
||||
return self.ip
|
||||
|
||||
@property
|
||||
def admin_user_display(self):
|
||||
|
@ -222,7 +216,7 @@ class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
|||
'private_key': auth_user.private_key_file
|
||||
}
|
||||
|
||||
if not with_become:
|
||||
if not with_become or self.is_windows():
|
||||
return info
|
||||
|
||||
if become_user:
|
||||
|
|
|
@ -29,7 +29,9 @@ class AuthBook(BaseUser, AbsConnectivity):
|
|||
permissions = [
|
||||
('test_authbook', _('Can test asset account connectivity')),
|
||||
('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):
|
||||
|
@ -123,8 +125,9 @@ class AuthBook(BaseUser, AbsConnectivity):
|
|||
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls):
|
||||
queryset = cls.objects.all() \
|
||||
def get_queryset(cls, is_history_model=False):
|
||||
model = cls.history.model if is_history_model else cls
|
||||
queryset = model.objects.all() \
|
||||
.annotate(ip=F('asset__ip')) \
|
||||
.annotate(hostname=F('asset__hostname')) \
|
||||
.annotate(platform=F('asset__platform__name')) \
|
||||
|
@ -134,3 +137,4 @@ class AuthBook(BaseUser, AbsConnectivity):
|
|||
def __str__(self):
|
||||
return self.smart_name
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from common.utils import (
|
|||
)
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from common.db import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
|
|
|
@ -165,24 +165,23 @@ class CommandFilterRule(OrgModelMixin):
|
|||
|
||||
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
|
||||
from tickets.const import TicketType
|
||||
from tickets.models import Ticket
|
||||
from tickets.models import ApplyCommandTicket
|
||||
data = {
|
||||
'title': _('Command confirm') + ' ({})'.format(session.user),
|
||||
'type': TicketType.command_confirm,
|
||||
'meta': {
|
||||
'apply_run_user': session.user,
|
||||
'apply_run_asset': session.asset,
|
||||
'apply_run_system_user': session.system_user,
|
||||
'apply_run_command': run_command,
|
||||
'apply_from_session_id': str(session.id),
|
||||
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
|
||||
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
|
||||
},
|
||||
'applicant': session.user_obj,
|
||||
'apply_run_user_id': session.user_id,
|
||||
'apply_run_asset': str(session.asset),
|
||||
'apply_run_system_user_id': session.system_user_id,
|
||||
'apply_run_command': run_command[:4090],
|
||||
'apply_from_session_id': str(session.id),
|
||||
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
|
||||
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(applicant=session.user_obj)
|
||||
ticket = ApplyCommandTicket.objects.create(**data)
|
||||
assignees = self.reviewers.all()
|
||||
ticket.open_by_system(assignees)
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -9,7 +9,7 @@ import paramiko
|
|||
from django.db import models
|
||||
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 .base import BaseUser
|
||||
|
||||
|
@ -36,7 +36,7 @@ class Domain(OrgModelMixin):
|
|||
def has_gateway(self):
|
||||
return self.gateway_set.filter(is_active=True).exists()
|
||||
|
||||
@property
|
||||
@lazyproperty
|
||||
def gateways(self):
|
||||
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]
|
||||
if 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)
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.db import models
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.const import Category, AllTypes
|
||||
from common.fields.model import JsonDictTextField
|
||||
from common.db.fields import JsonDictTextField
|
||||
|
||||
|
||||
__all__ = ['Platform']
|
||||
|
|
|
@ -7,16 +7,14 @@ import logging
|
|||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
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 common.utils import signer
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
from .authbook import AuthBook
|
||||
|
||||
|
||||
__all__ = ['AdminUser', 'SystemUser']
|
||||
__all__ = ['AdminUser', 'SystemUser', 'ProtocolMixin']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -70,151 +68,11 @@ class ProtocolMixin:
|
|||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
|
||||
class AuthMixin:
|
||||
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):
|
||||
class SystemUser(ProtocolMixin, BaseUser):
|
||||
LOGIN_AUTO = 'auto'
|
||||
LOGIN_MANUAL = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
(LOGIN_AUTO, _('Automatic managed')),
|
||||
(LOGIN_AUTO, _('使用账号')),
|
||||
(LOGIN_MANUAL, _('Manually input'))
|
||||
)
|
||||
|
||||
|
@ -226,24 +84,34 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
|||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField(
|
||||
'assets.Asset', blank=True, verbose_name=_("Assets"),
|
||||
through='assets.AuthBook', through_fields=['systemuser', 'asset'],
|
||||
related_name='system_users'
|
||||
)
|
||||
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
|
||||
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(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
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)]
|
||||
)
|
||||
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'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
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"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
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)
|
||||
ad_domain = models.CharField(default='', max_length=256)
|
||||
|
||||
# linux su 命令 (switch user)
|
||||
# Todo: 修改为 username, 不必系统用户了
|
||||
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"))
|
||||
|
||||
|
@ -262,7 +130,7 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
|||
return self.get_login_mode_display()
|
||||
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -11,5 +11,6 @@ from .cmd_filter import *
|
|||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .account import *
|
||||
from .platform import *
|
||||
from .account_history import *
|
||||
from .backup import *
|
||||
from .platform import *
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
from rest_framework import serializers
|
||||
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 .base import AuthSerializerMixin
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
|
||||
|
||||
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
ip = serializers.ReadOnlyField(label=_("IP"))
|
||||
hostname = serializers.ReadOnlyField(label=_("Hostname"))
|
||||
platform = serializers.ReadOnlyField(label=_("Platform"))
|
||||
protocols = serializers.SerializerMethodField(label=_("Protocols"))
|
||||
date_created = serializers.DateTimeField(
|
||||
label=_('Date created'), format="%Y/%m/%d %H:%M:%S", read_only=True
|
||||
)
|
||||
|
@ -22,22 +22,20 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
)
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields_mini = ['id', 'username', 'ip', 'hostname', 'platform', 'protocols', 'version']
|
||||
fields_write_only = ['password', 'private_key', "public_key", 'passphrase']
|
||||
model = Account
|
||||
fields_mini = [
|
||||
'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_small = fields_mini + fields_write_only + fields_other
|
||||
fields_fk = ['asset', 'systemuser', 'systemuser_display']
|
||||
fields_fk = ['asset']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {
|
||||
'write_only': True,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
'systemuser_display': {'label': _('System user display')}
|
||||
}
|
||||
ref_name = 'AssetAccountSerializer'
|
||||
|
||||
|
@ -56,21 +54,16 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
attrs = self._validate_gen_key(attrs)
|
||||
return attrs
|
||||
|
||||
def get_protocols(self, v):
|
||||
return v.protocols.replace(' ', ', ')
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" 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
|
||||
|
||||
def to_representation(self, instance):
|
||||
instance.load_auth()
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AccountSecretSerializer(AccountSerializer):
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
|
||||
|
|
|
@ -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
|
|
@ -3,9 +3,11 @@
|
|||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from ...models import Asset, Node, Platform, SystemUser
|
||||
from ..mixin import CategoryDisplayMixin
|
||||
from ..account import AccountSerializer
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
|
@ -76,6 +78,7 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
|
|||
platform_display = serializers.SlugField(
|
||||
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'
|
||||
]
|
||||
fields_m2m = [
|
||||
'nodes', 'nodes_display', 'labels', 'labels_display',
|
||||
'nodes', 'nodes_display', 'labels', 'labels_display', 'accounts'
|
||||
]
|
||||
read_only_fields = [
|
||||
'category', 'category_display', 'type', 'type_display',
|
||||
|
@ -110,6 +113,11 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
|
|||
'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):
|
||||
fields = super().get_fields()
|
||||
|
||||
|
@ -161,10 +169,24 @@ class AssetSerializer(CategoryDisplayMixin, OrgResourceModelSerializerMixin):
|
|||
nodes_to_set.append(node)
|
||||
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):
|
||||
self.compatible_with_old_protocol(validated_data)
|
||||
nodes_display = validated_data.pop('nodes_display', '')
|
||||
instance = super().create(validated_data)
|
||||
if self.accounts_data:
|
||||
self.add_accounts(instance, self.accounts_data)
|
||||
self.perform_nodes_display_create(instance, nodes_display)
|
||||
return instance
|
||||
|
||||
|
|
|
@ -6,12 +6,14 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework import serializers
|
||||
|
||||
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 .utils import validate_password_for_ansible
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
||||
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):
|
||||
if private_key is None:
|
||||
|
@ -31,6 +33,13 @@ class AuthSerializer(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(
|
||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||
write_only=True, label=_('Key password')
|
||||
|
|
|
@ -31,24 +31,24 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
|||
|
||||
|
||||
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display')
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display')
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display"))
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'content', 'ignore_case', 'pattern', 'priority',
|
||||
'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
'type', 'type_display', 'content', 'ignore_case', 'pattern',
|
||||
'priority', 'action', 'action_display', 'reviewers',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'date_created': {'label': _("Date created")},
|
||||
'date_updated': {'label': _("Date updated")},
|
||||
'action_display': {'label': _("Action display")}
|
||||
'action_display': {'label': _("Action display")},
|
||||
'pattern': {'label': _("Pattern")}
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from common.validators import alphanumeric
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from ..models import Domain, Gateway
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
|
@ -43,7 +44,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
|||
|
||||
|
||||
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
is_connective = serializers.BooleanField(required=False)
|
||||
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
|
@ -67,7 +68,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
|
||||
class Meta(GatewaySerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
|
|
|
@ -4,11 +4,13 @@ from django.db.models import Count
|
|||
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
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 orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.const import Protocol
|
||||
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
|
||||
|
||||
__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)
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
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(
|
||||
source='apps_amount', read_only=True, label=_('Apps amount')
|
||||
)
|
||||
|
@ -38,24 +48,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
fields_small = fields_mini + fields_write_only + [
|
||||
'token', 'ssh_key_fingerprint',
|
||||
'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',
|
||||
'username_same_with_user', 'auto_push', 'auto_generate_key',
|
||||
'username_same_with_user', 'auto_push_account', 'auto_generate_key',
|
||||
'su_enabled', 'su_from',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {
|
||||
"write_only": True,
|
||||
'trim_whitespace': False,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'cmd_filters': {"required": False, 'label': _('Command filter')},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'token': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Nodes amount')},
|
||||
'assets_amount': {'label': _('Assets amount')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
|
@ -188,7 +192,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
attrs['protocol'] = Protocol.ssh
|
||||
attrs['login_mode'] = SystemUser.LOGIN_AUTO
|
||||
attrs['username_same_with_user'] = False
|
||||
attrs['auto_push'] = False
|
||||
attrs['auto_push_account'] = False
|
||||
return attrs
|
||||
|
||||
def _validate_gen_key(self, attrs):
|
||||
|
@ -249,14 +253,14 @@ class MiniSystemUserSerializer(serializers.ModelSerializer):
|
|||
fields = SystemUserSerializer.Meta.fields_mini
|
||||
|
||||
|
||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'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',
|
||||
]
|
||||
fields = fields_small
|
||||
|
@ -265,6 +269,9 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
|||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'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'))
|
||||
|
||||
class Meta:
|
||||
model = SystemUser.assets.through
|
||||
model = SystemUser
|
||||
fields = [
|
||||
"id", "asset", "asset_display", 'systemuser', 'systemuser_display',
|
||||
"connectivity", 'date_verified', 'org_id'
|
||||
|
|
|
@ -2,8 +2,16 @@ from django.utils.translation import ugettext_lazy as _
|
|||
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
|
||||
# check password not contains `{{`
|
||||
# Ansible 推送的时候不支持
|
||||
if '{{' in password:
|
||||
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 `"` '))
|
||||
|
||||
|
|
|
@ -52,8 +52,6 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
|||
if not has_node:
|
||||
instance.nodes.add(Node.org_root())
|
||||
|
||||
instance.set_admin_user_relation()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
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
|
||||
).values_list('systemuser_id', 'asset_id'))
|
||||
# TODO 优化
|
||||
to_create = []
|
||||
for system_user_id in system_user_ids:
|
||||
asset_ids_to_push = []
|
||||
for asset_id in asset_ids:
|
||||
if (system_user_id, asset_id) in exist:
|
||||
continue
|
||||
asset_ids_to_push.append(asset_id)
|
||||
to_create.append(m2m_model(
|
||||
systemuser_id=system_user_id,
|
||||
asset_id=asset_id,
|
||||
org_id=instance.org_id
|
||||
))
|
||||
if asset_ids_to_push:
|
||||
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
|
||||
m2m_model.objects.bulk_create(to_create)
|
||||
|
||||
# to_create = []
|
||||
# for system_user_id in system_user_ids:
|
||||
# asset_ids_to_push = []
|
||||
# for asset_id in asset_ids:
|
||||
# if (system_user_id, asset_id) in exist:
|
||||
# continue
|
||||
# asset_ids_to_push.append(asset_id)
|
||||
# to_create.append(m2m_model(
|
||||
# systemuser_id=system_user_id,
|
||||
# asset_id=asset_id,
|
||||
# org_id=instance.org_id
|
||||
# ))
|
||||
# if asset_ids_to_push:
|
||||
# push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
|
||||
# m2m_model.objects.bulk_create(to_create)
|
||||
#
|
||||
|
||||
RELATED_NODE_IDS = '_related_node_ids'
|
||||
|
||||
|
|
|
@ -1,50 +1,14 @@
|
|||
from django.dispatch import receiver
|
||||
from django.apps import apps
|
||||
from simple_history.signals import pre_create_historical_record
|
||||
from django.db.models.signals import post_save, pre_save, pre_delete
|
||||
from django.db.models.signals import pre_save
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
@receiver(pre_create_historical_record, sender=AuthBookHistory)
|
||||
def pre_create_historical_record_callback(sender, history_instance=None, **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):
|
||||
@receiver(pre_save, sender=Account)
|
||||
def on_account_pre_create(sender, instance, **kwargs):
|
||||
# 升级版本号
|
||||
instance.version += 1
|
||||
# 即使在 root 组织也不怕
|
||||
|
|
|
@ -9,9 +9,8 @@ from common.exceptions import M2MReverseNotAllowed
|
|||
from common.const.signals import POST_ADD
|
||||
from common.utils import get_logger
|
||||
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 orgs.utils import tmp_to_root_org
|
||||
from assets.tasks import (
|
||||
push_system_user_to_assets_manual,
|
||||
push_system_user_to_assets,
|
||||
|
@ -39,35 +38,7 @@ def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
|
|||
else:
|
||||
system_user_ids = pk_set
|
||||
asset_ids = [instance.id]
|
||||
|
||||
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)
|
||||
# todo: Auto create account if need
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.users.through)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import os
|
||||
import time
|
||||
import pandas as pd
|
||||
from openpyxl import Workbook
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.models import AuthBook
|
||||
from assets.models import Account
|
||||
from assets.serializers import AccountSecretSerializer
|
||||
from assets.notifications import AccountBackupExecutionTaskMsg
|
||||
from applications.models import Account
|
||||
|
@ -48,7 +48,7 @@ class BaseAccountHandler:
|
|||
_fields = cls.get_header_fields(v)
|
||||
header_fields.update(_fields)
|
||||
else:
|
||||
header_fields[field] = v.label
|
||||
header_fields[field] = str(v.label)
|
||||
return header_fields
|
||||
|
||||
@classmethod
|
||||
|
@ -59,7 +59,7 @@ class BaseAccountHandler:
|
|||
data = cls.unpack_data(serializer.data)
|
||||
row_dict = {}
|
||||
for field, header_name in header_fields.items():
|
||||
row_dict[header_name] = data[field]
|
||||
row_dict[header_name] = str(data[field])
|
||||
return row_dict
|
||||
|
||||
|
||||
|
@ -72,24 +72,24 @@ class AssetAccountHandler(BaseAccountHandler):
|
|||
return filename
|
||||
|
||||
@classmethod
|
||||
def create_df(cls):
|
||||
df_dict = defaultdict(list)
|
||||
sheet_name = AuthBook._meta.verbose_name
|
||||
def create_data_map(cls):
|
||||
data_map = defaultdict(list)
|
||||
sheet_name = Account._meta.verbose_name
|
||||
|
||||
accounts = AuthBook.get_queryset().select_related('systemuser')
|
||||
accounts = Account.get_queryset()
|
||||
if not accounts.first():
|
||||
return df_dict
|
||||
return data_map
|
||||
|
||||
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
row = cls.create_row(account, AccountSecretSerializer, header_fields)
|
||||
df_dict[sheet_name].append(row)
|
||||
for k, v in df_dict.items():
|
||||
df_dict[k] = pd.DataFrame(v)
|
||||
if sheet_name not in data_map:
|
||||
data_map[sheet_name].append(list(row.keys()))
|
||||
data_map[sheet_name].append(list(row.values()))
|
||||
|
||||
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
|
||||
return df_dict
|
||||
return data_map
|
||||
|
||||
|
||||
class AppAccountHandler(BaseAccountHandler):
|
||||
|
@ -101,19 +101,19 @@ class AppAccountHandler(BaseAccountHandler):
|
|||
return filename
|
||||
|
||||
@classmethod
|
||||
def create_df(cls):
|
||||
df_dict = defaultdict(list)
|
||||
def create_data_map(cls):
|
||||
data_map = defaultdict(list)
|
||||
accounts = Account.get_queryset().select_related('systemuser')
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
app_type = account.type
|
||||
sheet_name = AppType.get_label(app_type)
|
||||
row = cls.create_row(account, AppAccountSecretSerializer)
|
||||
df_dict[sheet_name].append(row)
|
||||
for k, v in df_dict.items():
|
||||
df_dict[k] = pd.DataFrame(v)
|
||||
if sheet_name not in data_map:
|
||||
data_map[sheet_name].append(list(row.keys()))
|
||||
data_map[sheet_name].append(list(row.values()))
|
||||
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
|
||||
return df_dict
|
||||
return data_map
|
||||
|
||||
|
||||
handler_map = {
|
||||
|
@ -142,24 +142,24 @@ class AccountBackupHandler:
|
|||
if not handler:
|
||||
continue
|
||||
|
||||
df_dict = handler.create_df()
|
||||
if not df_dict:
|
||||
data_map = handler.create_data_map()
|
||||
if not data_map:
|
||||
continue
|
||||
|
||||
filename = handler.get_filename(self.plan_name)
|
||||
with pd.ExcelWriter(filename) as w:
|
||||
for sheet, df in df_dict.items():
|
||||
sheet = sheet.replace(' ', '-')
|
||||
getattr(df, 'to_excel')(w, sheet_name=sheet, index=False)
|
||||
|
||||
wb = Workbook(filename)
|
||||
for sheet, data in data_map.items():
|
||||
ws = wb.create_sheet(str(sheet))
|
||||
for row in data:
|
||||
ws.append(row)
|
||||
wb.save(filename)
|
||||
files.append(filename)
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
logger.info('步骤完成: 用时 {}s'.format(timedelta))
|
||||
return files
|
||||
|
||||
def send_backup_mail(self, files):
|
||||
recipients = self.execution.plan_snapshot.get('recipients')
|
||||
if not recipients:
|
||||
return
|
||||
def send_backup_mail(self, files, recipients):
|
||||
if not files:
|
||||
return
|
||||
recipients = User.objects.filter(id__in=list(recipients))
|
||||
|
@ -198,8 +198,16 @@ class AccountBackupHandler:
|
|||
is_success = False
|
||||
error = '-'
|
||||
try:
|
||||
files = self.create_excel()
|
||||
self.send_backup_mail(files)
|
||||
recipients = self.execution.plan_snapshot.get('recipients')
|
||||
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:
|
||||
self.is_frozen = True
|
||||
logger.error('任务执行被异常中断')
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext_noop
|
|||
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
from ..models import Asset, Connectivity, AuthBook
|
||||
from ..models import Asset, Connectivity, Account
|
||||
from . import const
|
||||
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):
|
||||
asset_ids_ok = 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_failed, Connectivity.failed)
|
||||
|
||||
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin')
|
||||
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin')
|
||||
accounts_ok = Account.objects.filter(asset_id__in=asset_ids_ok,)
|
||||
accounts_failed = Account.objects.filter(asset_id__in=asset_ids_faile)
|
||||
|
||||
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok)
|
||||
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed)
|
||||
Account.bulk_set_connectivity(accounts_ok, Connectivity.ok)
|
||||
Account.bulk_set_connectivity(accounts_failed, Connectivity.failed)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
|
|
|
@ -9,6 +9,7 @@ from assets.models import AuthBook
|
|||
__all__ = ['add_nodes_assets_to_system_users']
|
||||
|
||||
|
||||
# Todo: 等待优化
|
||||
@shared_task
|
||||
@tmp_to_root_org()
|
||||
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)
|
||||
assets = Node.get_nodes_all_assets(*nodes)
|
||||
|
||||
for system_user in system_users:
|
||||
""" 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号,
|
||||
无法更新节点下所有资产的管理用户的问题 """
|
||||
|
@ -28,7 +30,7 @@ def add_nodes_assets_to_system_users(nodes_keys, system_users):
|
|||
)
|
||||
if created:
|
||||
need_push_asset_ids.append(asset.id)
|
||||
# # 不再自动更新资产管理用户,只允许用户手动指定。
|
||||
# 不再自动更新资产管理用户,只允许用户手动指定。
|
||||
# 只要关联都需要更新资产的管理用户
|
||||
# instance.update_asset_admin_user_if_need()
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
comment = system_user.name
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
|
||||
algorithm = kwargs.get('algorithm')
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
|
||||
comment = system_user.name
|
||||
if system_user.username_same_with_user:
|
||||
from users.models import User
|
||||
user = User.objects.filter(username=username).only('name', 'username').first()
|
||||
if user:
|
||||
comment = f'{system_user.name}[{str(user)}]'
|
||||
comment = comment.replace(' ', '')
|
||||
|
||||
password = system_user.password
|
||||
public_key = system_user.public_key
|
||||
|
@ -104,7 +105,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
|||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present password={}'.format(
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
|
@ -176,7 +177,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
|
|||
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 命令,跟资产无关
|
||||
: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)
|
||||
if not system_user.username_same_with_user:
|
||||
return get_tasks(system_user)
|
||||
return get_tasks(system_user, algorithm=algorithm)
|
||||
tasks = []
|
||||
# 仅推送这个username
|
||||
if username is not None:
|
||||
tasks.extend(get_tasks(system_user, username))
|
||||
tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
|
||||
return tasks
|
||||
users = system_user.users.all().values_list('username', flat=True)
|
||||
print(_("System user is dynamic: {}").format(list(users)))
|
||||
for _username in users:
|
||||
tasks.extend(get_tasks(system_user, _username))
|
||||
tasks.extend(get_tasks(system_user, _username, algorithm=algorithm))
|
||||
return tasks
|
||||
|
||||
|
||||
|
@ -244,7 +245,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
|||
for u in usernames:
|
||||
for a in _assets:
|
||||
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])
|
||||
|
||||
|
||||
|
@ -269,7 +274,7 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
|
|||
# if username is None:
|
||||
# username = system_user.username
|
||||
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)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.utils.translation import ugettext as _, gettext_noop
|
|||
from assets.models import Asset
|
||||
from common.utils import get_logger
|
||||
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 .utils import (
|
||||
clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
@ -34,11 +34,11 @@ def set_assets_accounts_connectivity(system_user, assets, results_summary):
|
|||
else:
|
||||
asset_ids_failed.add(asset.id)
|
||||
|
||||
accounts_ok = AuthBook.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_ok = Account.objects.filter(asset_id__in=asset_ids_ok, 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)
|
||||
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed)
|
||||
Account.bulk_set_connectivity(accounts_ok, Connectivity.ok)
|
||||
Account.bulk_set_connectivity(accounts_failed, Connectivity.failed)
|
||||
|
||||
|
||||
@org_aware_func("system_user")
|
||||
|
|
|
@ -25,7 +25,7 @@ def check_asset_can_run_ansible(asset):
|
|||
|
||||
|
||||
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}')
|
||||
return False
|
||||
if not system_user.is_protocol_support_push:
|
||||
|
|
|
@ -15,6 +15,8 @@ router.register(r'hosts', api.HostViewSet, 'host')
|
|||
router.register(r'databases', api.DatabaseViewSet, 'database')
|
||||
router.register(r'accounts', api.AccountViewSet, 'account')
|
||||
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'system-users', api.SystemUserViewSet, 'system-user')
|
||||
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
|
||||
|
|
|
@ -3,3 +3,23 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
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',
|
||||
)
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
|
|||
|
||||
class OperateLog(OrgModelMixin):
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_VIEW = 'view'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, _("Create")),
|
||||
(ACTION_VIEW, _("View")),
|
||||
(ACTION_UPDATE, _("Update")),
|
||||
(ACTION_DELETE, _("Delete"))
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_delete
|
||||
)
|
||||
|
@ -21,7 +23,7 @@ from jumpserver.utils import current_request
|
|||
from users.models import User
|
||||
from users.signals import post_user_change_password
|
||||
from terminal.models import Session, Command
|
||||
from .utils import write_login_log
|
||||
from .utils import write_login_log, create_operate_log
|
||||
from . import models, serializers
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
|
@ -36,26 +38,6 @@ logger = get_logger(__name__)
|
|||
sys_logger = get_syslogger(__name__)
|
||||
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):
|
||||
@staticmethod
|
||||
|
@ -69,7 +51,9 @@ class AuthBackendLabelMapping(LazyObject):
|
|||
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
|
||||
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_TEMP_TOKEN] = _('Temporary token')
|
||||
return backend_label_mapping
|
||||
|
||||
def _setup(self):
|
||||
|
@ -79,28 +63,6 @@ class AuthBackendLabelMapping(LazyObject):
|
|||
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 = {
|
||||
User.groups.through._meta.object_name: (
|
||||
_('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))
|
||||
check_different_city_login_if_need(user, request)
|
||||
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})
|
||||
write_login_log(**data)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from celery import shared_task
|
|||
from ops.celery.decorator import (
|
||||
register_as_period_task
|
||||
)
|
||||
from .models import UserLoginLog, OperateLog
|
||||
from .models import UserLoginLog, OperateLog, FTPLog
|
||||
from common.utils import get_log_keep_day
|
||||
|
||||
|
||||
|
@ -29,7 +29,7 @@ def clean_ftp_log_period():
|
|||
now = timezone.now()
|
||||
days = get_log_keep_day('FTP_LOG_KEEP_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)
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import csv
|
||||
import codecs
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .const import DEFAULT_CITY
|
||||
from common.utils import validate_ip, get_ip_city
|
||||
from django.http import HttpResponse
|
||||
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):
|
||||
|
@ -36,3 +44,25 @@ def write_login_log(*args, **kwargs):
|
|||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
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))
|
||||
|
|
|
@ -5,9 +5,11 @@ from .connection_token import *
|
|||
from .token import *
|
||||
from .mfa import *
|
||||
from .access_key import *
|
||||
from .confirm import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
from .wecom import *
|
||||
from .dingtalk import *
|
||||
from .feishu import *
|
||||
from .password import *
|
||||
from .temp_token import *
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
#
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from .. import serializers
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
|
||||
class AccessKeyViewSet(ModelViewSet):
|
||||
serializer_class = serializers.AccessKeySerializer
|
||||
search_fields = ['^id', '^secret']
|
||||
permission_classes = [RBACPermission]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.user.access_keys.all()
|
||||
|
|
|
@ -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)
|
|
@ -1,73 +1,98 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import urllib.parse
|
||||
import json
|
||||
from typing import Callable
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import ctypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
import urllib.parse
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
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 django.shortcuts import get_object_or_404
|
||||
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 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.drf.api import JMSModelViewSet
|
||||
from common.http import is_true
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from perms.models.base import Action
|
||||
from perms.utils.application.permission import get_application_actions
|
||||
from perms.utils.asset.permission import get_asset_actions
|
||||
from common.const.http import PATCH
|
||||
from terminal.models import EndpointRule
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer,
|
||||
ConnectionTokenDisplaySerializer,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserConnectionTokenViewSet']
|
||||
from ..models import ConnectionToken
|
||||
|
||||
|
||||
class ClientProtocolMixin:
|
||||
"""
|
||||
下载客户端支持的连接文件,里面包含了 token,和 其他连接信息
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
|
||||
- [x] RDP
|
||||
- [ ] KoKo
|
||||
|
||||
本质上,这里还是暴露出 token 来,进行使用
|
||||
"""
|
||||
class ConnectionTokenMixin:
|
||||
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')
|
||||
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')
|
||||
if not user or not self.request.user.is_superuser:
|
||||
user = self.request.user
|
||||
return asset, application, system_user, user
|
||||
@staticmethod
|
||||
def check_user_has_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
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
|
||||
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
|
||||
|
||||
def get_rdp_file_content(self, serializer):
|
||||
options = {
|
||||
def get_client_protocol_data(self, token: ConnectionToken):
|
||||
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': '',
|
||||
'username:s': '',
|
||||
# 'screen mode id:i': '1',
|
||||
|
@ -93,419 +118,192 @@ class ClientProtocolMixin:
|
|||
'bookmarktype:i': '3',
|
||||
'use redirection server name:i': '0',
|
||||
'smart sizing:i': '1',
|
||||
#'drivestoredirect:s': '*',
|
||||
# 'drivestoredirect:s': '*',
|
||||
# 'domain:s': ''
|
||||
# 'alternate shell:s:': '||MySQLWorkbench',
|
||||
# 'remoteapplicationname:s': 'Firefox',
|
||||
# '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')
|
||||
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:
|
||||
options['desktopwidth:i'] = width
|
||||
options['desktopheight:i'] = height
|
||||
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
|
||||
rdp_options['desktopwidth:i'] = width
|
||||
rdp_options['desktopheight:i'] = 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:
|
||||
name = asset.hostname
|
||||
elif application:
|
||||
name = application.name
|
||||
application.get_rdp_remote_app_setting()
|
||||
|
||||
app = f'||jmservisor'
|
||||
options['remoteapplicationmode:i'] = '1'
|
||||
options['alternate shell:s'] = app
|
||||
options['remoteapplicationprogram:s'] = app
|
||||
options['remoteapplicationname:s'] = name
|
||||
options['remoteapplicationcmdline:s'] = '- ' + self.get_encrypt_cmdline(application)
|
||||
if token.asset:
|
||||
name = token.asset.hostname
|
||||
elif token.application and token.application.category_remote_app:
|
||||
app = '||jmservisor'
|
||||
name = token.application.name
|
||||
rdp_options['remoteapplicationmode:i'] = '1'
|
||||
rdp_options['alternate shell:s'] = app
|
||||
rdp_options['remoteapplicationprogram:s'] = app
|
||||
rdp_options['remoteapplicationname:s'] = name
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
filename = "{}-{}-jumpserver".format(token.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
|
||||
content = ''
|
||||
for k, v in options.items():
|
||||
for k, v in rdp_options.items():
|
||||
content += f'{k}:{v}\n'
|
||||
return name, content
|
||||
|
||||
def get_ssh_token(self, serializer):
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
if asset:
|
||||
name = asset.hostname
|
||||
elif application:
|
||||
name = application.name
|
||||
return filename, content
|
||||
|
||||
def get_ssh_token(self, token: ConnectionToken):
|
||||
if token.asset:
|
||||
name = token.asset.hostname
|
||||
elif token.application:
|
||||
name = token.application.name
|
||||
else:
|
||||
name = '*'
|
||||
filename = f'{token.user.username}-{name}-jumpserver'
|
||||
|
||||
content = {
|
||||
'ip': settings.TERMINAL_KOKO_HOST,
|
||||
'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
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='ssh', asset=token.asset, application=token.application
|
||||
)
|
||||
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 = {
|
||||
'ok': ok,
|
||||
'msg': msg
|
||||
'ip': endpoint.host,
|
||||
'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(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
SecretDetailMixin, TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
|
||||
filterset_fields = (
|
||||
'type',
|
||||
'user_display', 'system_user_display', 'application_display', 'asset_display'
|
||||
)
|
||||
search_fields = filterset_fields
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'list': ConnectionTokenDisplaySerializer,
|
||||
'retrieve': ConnectionTokenDisplaySerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'retrieve': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken',
|
||||
'expire': 'authentication.add_connectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
queryset = ConnectionToken.objects.all()
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
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)
|
||||
def create_connection_token(self):
|
||||
data = self.request.query_params if self.request.method == 'GET' else self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
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)
|
||||
token, secret = self.create_token(user, asset, application, system_user)
|
||||
tp = 'app' if application else 'asset'
|
||||
def perform_create(self, serializer):
|
||||
user, asset, application, system_user = self.get_request_resources(serializer)
|
||||
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 = {
|
||||
"id": token, 'secret': secret,
|
||||
'type': tp, 'protocol': system_user.protocol
|
||||
'url': 'jms://{}'.format(protocol_data)
|
||||
}
|
||||
return Response(data, status=201)
|
||||
return Response(data=data)
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser, Asset
|
||||
from applications.models import Application
|
||||
from perms.utils.asset.permission import validate_permission as asset_validate_permission
|
||||
from perms.utils.application.permission import validate_permission as app_validate_permission
|
||||
@action(methods=['PATCH'], detail=True)
|
||||
def expire(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.expire()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
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'))
|
||||
if not user.is_valid:
|
||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
||||
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
serializer_classes = {
|
||||
'default': SuperConnectionTokenSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'create': 'authentication.add_superconnectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken'
|
||||
}
|
||||
|
||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||
asset = None
|
||||
app = None
|
||||
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)
|
||||
@action(methods=['PATCH'], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
from common.utils.timezone import as_current_tz
|
||||
|
||||
if not has_perm:
|
||||
raise serializers.ValidationError('Permission expired or invalid')
|
||||
return value, user, system_user, asset, app, expired_at, actions
|
||||
token_id = request.data.get('token') or ''
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
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)
|
||||
|
|
|
@ -2,10 +2,11 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import UserConfirmation
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -26,9 +27,8 @@ class DingTalkQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
|
@ -2,10 +2,11 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import UserConfirmation
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -26,7 +27,7 @@ class FeiShuQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
|
|
|
@ -25,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
|||
ticket = self.get_ticket()
|
||||
if ticket:
|
||||
request.session.pop('auth_ticket_id', '')
|
||||
ticket.close(processor=self.get_user_from_session())
|
||||
ticket.close()
|
||||
return Response('', status=200)
|
||||
|
|
|
@ -10,22 +10,17 @@ from rest_framework.generics import CreateAPIView
|
|||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser, NeedMFAVerify
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import UnexpectError
|
||||
from users.models.user import User
|
||||
from ..serializers import OtpVerifySerializer
|
||||
from .. import serializers
|
||||
from .. import errors
|
||||
from ..mfa.otp import MFAOtp
|
||||
from ..mixins import AuthMixin
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'MFAChallengeVerifyApi', 'UserOtpVerifyApi',
|
||||
'MFASendCodeApi'
|
||||
'MFAChallengeVerifyApi', 'MFASendCodeApi'
|
||||
]
|
||||
|
||||
|
||||
|
@ -88,30 +83,3 @@ class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
|
|||
raise ValidationError(data)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
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()
|
||||
|
|
|
@ -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)
|
|
@ -1,6 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.shortcuts import redirect
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
@ -28,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
|||
def create(self, request, *args, **kwargs):
|
||||
self.create_session_if_need()
|
||||
# 如果认证没有过,检查账号密码
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
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_login_confirm_if_need(user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
|
|
|
@ -2,10 +2,11 @@ from rest_framework.views import APIView
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import UserConfirmation
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication.const import ConfirmType
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -26,9 +27,8 @@ class WeComQRUnBindBase(APIView):
|
|||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
from django.contrib.auth.backends import BaseBackend
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
|
|
|
@ -198,6 +198,6 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
|||
return None, None
|
||||
user, secret = key.user, str(key.secret)
|
||||
return user, secret
|
||||
except AccessKey.DoesNotExist:
|
||||
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
||||
return None, None
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
|||
else:
|
||||
built = False
|
||||
|
||||
return (user, built)
|
||||
return user, built
|
||||
|
||||
def pre_check(self, username, password):
|
||||
if not settings.AUTH_LDAP:
|
||||
|
@ -75,6 +75,9 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
|||
|
||||
def authenticate(self, request=None, username=None, password=None, **kwargs):
|
||||
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)
|
||||
if not match:
|
||||
logger.info('Authenticate failed: {}'.format(msg))
|
||||
|
@ -154,6 +157,8 @@ class LDAPUser(_LDAPUser):
|
|||
|
||||
def _populate_user_from_attributes(self):
|
||||
for field, attr in self.settings.USER_ATTR_MAP.items():
|
||||
if field in ['groups']:
|
||||
continue
|
||||
try:
|
||||
value = self.attrs[attr][0]
|
||||
value = value.strip()
|
||||
|
|
|
@ -18,6 +18,7 @@ from django.urls import reverse
|
|||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
|
@ -39,17 +40,22 @@ class UserMixin:
|
|||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
sub = claims['sub']
|
||||
name = claims.get('name', sub)
|
||||
username = claims.get('preferred_username', sub)
|
||||
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
|
||||
logger.debug(
|
||||
log_prompt.format(
|
||||
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
|
||||
)
|
||||
)
|
||||
|
||||
# Construct user attrs value
|
||||
user_attrs = {}
|
||||
for field, attr in settings.AUTH_OPENID_USER_ATTR_MAP.items():
|
||||
user_attrs[field] = claims.get(attr, sub)
|
||||
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(
|
||||
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("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
|
||||
# token endpoint of the OIDC provider.
|
||||
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 = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
# 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())}
|
||||
if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post':
|
||||
token_payload.update({
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
})
|
||||
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.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
|
@ -258,6 +287,11 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
|||
try:
|
||||
claims_response.raise_for_status()
|
||||
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:
|
||||
error = "Json claims response error, claims response " \
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
|
@ -286,5 +320,3 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
|||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid"
|
||||
)
|
||||
return None
|
||||
|
||||
|
|
|
@ -13,20 +13,23 @@ User = get_user_model()
|
|||
|
||||
|
||||
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):
|
||||
username = username.decode()
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
if '@' in username:
|
||||
email = username
|
||||
else:
|
||||
email_suffix = settings.EMAIL_SUFFIX
|
||||
email = '{}@{}'.format(username, email_suffix)
|
||||
user = User(username=username, name=username, email=email)
|
||||
user.source = user.Source.radius.value
|
||||
user.save()
|
||||
user = User.objects.filter(username=username).first()
|
||||
if user:
|
||||
return user
|
||||
|
||||
if '@' in username:
|
||||
email = username
|
||||
else:
|
||||
email_suffix = settings.EMAIL_SUFFIX
|
||||
email = '{}@{}'.format(username, email_suffix)
|
||||
|
||||
user = User(username=username, name=username, email=email)
|
||||
user.source = user.Source.radius.value
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def _perform_radius_auth(self, client, packet):
|
||||
|
|
|
@ -14,7 +14,7 @@ from ..base import JMSModelBackend
|
|||
|
||||
__all__ = ['SAML2Backend']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SAML2Backend(JMSModelBackend):
|
||||
|
|
|
@ -74,27 +74,37 @@ class PrepareRequestMixin:
|
|||
return idp_settings
|
||||
|
||||
@staticmethod
|
||||
def get_attribute_consuming_service():
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
if attr_mapping and isinstance(attr_mapping, dict):
|
||||
attr_list = [
|
||||
{
|
||||
"name": sp_key,
|
||||
"friendlyName": idp_key, "isRequired": True
|
||||
}
|
||||
for idp_key, sp_key in attr_mapping.items()
|
||||
]
|
||||
request_attribute_template = {
|
||||
"attributeConsumingService": {
|
||||
"isDefault": False,
|
||||
"serviceName": "JumpServer",
|
||||
"serviceDescription": "JumpServer",
|
||||
"requestedAttributes": attr_list
|
||||
}
|
||||
def get_request_attributes():
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES or {}
|
||||
attr_map_reverse = {v: k for k, v in attr_mapping.items()}
|
||||
need_attrs = (
|
||||
('username', 'username', True),
|
||||
('email', 'email', True),
|
||||
('name', 'name', False),
|
||||
('phone', 'phone', False),
|
||||
('comment', 'comment', False),
|
||||
)
|
||||
attr_list = []
|
||||
for name, friend_name, is_required in need_attrs:
|
||||
rename_name = attr_map_reverse.get(friend_name)
|
||||
name = rename_name if rename_name else name
|
||||
attr_list.append({
|
||||
"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 {}
|
||||
}
|
||||
return request_attribute_template
|
||||
|
||||
@staticmethod
|
||||
def get_advanced_settings():
|
||||
|
@ -167,11 +177,14 @@ class PrepareRequestMixin:
|
|||
|
||||
def get_attributes(self, saml_instance):
|
||||
user_attrs = {}
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
attrs = saml_instance.get_attributes()
|
||||
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
|
||||
|
||||
for attr, value in attrs.items():
|
||||
attr = attr.rsplit('/', 1)[-1]
|
||||
if attr_mapping and attr_mapping.get(attr):
|
||||
attr = attr_mapping.get(attr)
|
||||
if attr not in valid_attrs:
|
||||
continue
|
||||
user_attrs[attr] = self.value_to_str(value)
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
from .mfa import ConfirmMFA
|
||||
from .password import ConfirmPassword
|
||||
from .relogin import ConfirmReLogin
|
||||
|
||||
CONFIRM_BACKENDS = [ConfirmReLogin, ConfirmPassword, ConfirmMFA]
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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, ''
|
|
@ -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_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
|
||||
|
|
|
@ -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')
|
|
@ -0,0 +1,4 @@
|
|||
from .const import *
|
||||
from .mfa import *
|
||||
from .failed import *
|
||||
from .redirect import *
|
|
@ -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 {}")
|
|
@ -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')
|
|
@ -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
Loading…
Reference in New Issue