perf: merge with dev

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

1
.gitattributes vendored
View File

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

3
.gitignore vendored
View File

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

View File

@ -1,20 +1,5 @@
# 编译代码
FROM python:3.8-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
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

View File

@ -1,10 +1,13 @@
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
<p align="center">
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
</p>
<h3 align="center">多云环境下更好用的堡垒机</h3>
<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.

View File

@ -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},

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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

View File

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

View File

@ -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')),

View File

@ -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')),

View File

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

View File

@ -44,6 +44,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_remote_app(self):
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

View File

@ -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',

View File

@ -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
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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
)

View File

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

View File

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

View File

@ -1,4 +1,4 @@
from django.db.models import F, Q
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django_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

View File

@ -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},

View File

@ -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)

View File

@ -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 = [

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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')),
],

View File

@ -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')),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from 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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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']

View File

@ -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

View File

@ -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 *

View File

@ -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',

View File

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

View File

@ -3,9 +3,11 @@
from rest_framework import serializers
from 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

View File

@ -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')

View File

@ -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):

View File

@ -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},

View File

@ -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'

View File

@ -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 `"` '))

View File

@ -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'

View File

@ -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 组织也不怕

View File

@ -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)

View File

@ -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('任务执行被异常中断')

View File

@ -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")

View File

@ -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()

View File

@ -32,17 +32,18 @@ def _dump_args(args: dict):
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
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)

View File

@ -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")

View File

@ -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:

View File

@ -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')

View File

@ -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',
)

View File

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

View File

@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
class OperateLog(OrgModelMixin):
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"))
)

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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 *

View File

@ -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()

View File

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

View File

@ -1,73 +1,98 @@
# -*- coding: utf-8 -*-
#
import urllib.parse
import json
from typing import Callable
import os
import 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)

View File

@ -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'

View File

@ -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):

View File

@ -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)

View File

@ -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()

View File

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

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
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)

View File

@ -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'

View File

@ -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__)

View 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

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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