Compare commits

..

7 Commits

Author SHA1 Message Date
Jiangjie.Bai
622bef07ad fix: 修复获取类型为null的命令显示不支持的问题
fix: 修复获取类型为null的命令显示不支持的问题
2022-05-07 17:56:33 +08:00
fit2bot
a23bd4b3eb fix: 修复system-role获取users失败的问题 (#8197)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-05-07 10:40:01 +08:00
Jiangjie.Bai
9bd9d443b4 fix: 修复windows执行ansible显示sudo失败的问题 2022-05-05 11:40:37 +08:00
feng626
e56fc93a6e fix: 组织管理员 添加 view platform perm 2022-04-28 19:10:15 +08:00
fit2bot
eb17183d97 fix: workbench_orgs 去重 (#8151)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-25 11:39:51 +08:00
feng626
b0057ecb9d perf: client download 2022-04-24 15:09:50 +08:00
ibuler
a141a8d2c2 fix: 修复社区版跳转问题 2022-04-21 22:48:34 +08:00
401 changed files with 7184 additions and 11502 deletions

1
.gitattributes vendored
View File

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

18
.github/workflows/lgtm.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Send LGTM reaction
on:
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1.0.0
- uses: micnncim/action-lgtm-reaction@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
trigger: '["^.?lgtm$"]'

3
.gitignore vendored
View File

@@ -31,13 +31,12 @@ media
celerybeat.pid
django.db
celerybeat-schedule.db
data/static
docs/_build/
xpack
xpack.bak
logs/*
### Vagrant ###
.vagrant/
release/*
releashe
/apps/script.py
data/*

View File

@@ -1,5 +1,20 @@
FROM python:3.8-slim
# 编译代码
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
ARG BUILD_DEPENDENCIES=" \
g++ \
@@ -29,61 +44,39 @@ ARG TOOLS=" \
redis-tools \
telnet \
vim \
unzip \
wget"
RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
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 -y install ${BUILD_DEPENDENCIES} \
&& apt -y install ${DEPENDENCIES} \
&& apt -y install ${TOOLS} \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* \
&& mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh
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 \
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 \
&& ldconfig \
&& rm -f ${ORACLE_FILE}
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
WORKDIR /tmp/build
COPY ./requirements ./requirements
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
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
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
RUN echo > config.yml \
&& 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,13 +1,10 @@
<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>
@@ -16,9 +13,9 @@
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
JumpServer 是全球首款开源堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
@@ -31,9 +28,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 开源: 零门槛,线上快速获取和安装;
- 分布式: 轻松支持大规模并发访问;
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
- 多租户: 一套系统,多个子公司或部门同时使用;
- 多云支持: 一套系统,同时管理不同云上面的资产;
- 云端存储: 审计录像云端存储,永不丢失;
- 多租户: 一套系统,多个子公司和部门同时使用;
- 多应用支持: 数据库Windows远程应用Kubernetes。
### UI 展示
@@ -58,15 +55,12 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [手动安装](https://github.com/jumpserver/installer)
### 组件项目
| 项目 | 状态 | 描述 |
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
| [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 安装包 项目 |
- [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 安装包 项目
### 社区
@@ -81,13 +75,27 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
感谢以下贡献者,让 JumpServer 更加完善
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
<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>
### 致谢
- [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 企业版
@@ -95,18 +103,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
### 案例研究
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
- [万华化学:通过JumpServer管理全球化分布式IT资产并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
- [携程JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [大智慧JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [小红书:的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)
### 安全说明
@@ -127,3 +131,4 @@ 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_step.ticket_assignees.all()
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@@ -44,49 +44,85 @@ class LoginACL(BaseACL):
def __str__(self):
return self.name
def is_action(self, action):
return self.action == action
@property
def action_reject(self):
return self.action == self.ActionChoices.reject
@property
def action_allow(self):
return self.action == self.ActionChoices.allow
@classmethod
def filter_acl(cls, user):
return user.login_acls.all().valid().distinct()
@staticmethod
def match(user, ip):
acls = LoginACL.filter_acl(user)
if not acls:
return
def allow_user_confirm_if_need(user, ip):
acl = LoginACL.filter_acl(user).filter(
action=LoginACL.ActionChoices.confirm
).first()
acl = acl if acl and acl.reviewers.exists() else None
if not acl:
return False, acl
ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period')
is_contain_ip = contains_ip(ip, ip_group)
is_contain_time_period = contains_time_period(time_periods)
return is_contain_ip and is_contain_time_period, acl
for acl in acls:
if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists():
continue
ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period')
is_contain_ip = contains_ip(ip, ip_group)
is_contain_time_period = contains_time_period(time_periods)
if is_contain_ip and is_contain_time_period:
# 满足条件,则返回
return acl
@staticmethod
def allow_user_to_login(user, ip):
acl = LoginACL.filter_acl(user).exclude(
action=LoginACL.ActionChoices.confirm
).first()
if not acl:
return True, ''
ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period')
is_contain_ip = contains_ip(ip, ip_group)
is_contain_time_period = contains_time_period(time_periods)
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)
reject_type = ''
if is_contain_ip and is_contain_time_period:
# 满足条件
allow = acl.action_allow
if not allow:
reject_type = 'ip' if is_contain_ip else 'time'
else:
# 不满足条件
# 如果acl本身允许那就拒绝如果本身拒绝那就允许
allow = not acl.action_allow
if not allow:
reject_type = 'ip' if not is_contain_ip else 'time'
return allow, reject_type
@staticmethod
def construct_confirm_ticket_meta(request=None):
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()
data = {
'title': title,
'type': const.TicketType.login_confirm,
'applicant': self.user,
'apply_login_city': login_city,
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,
'org_id': Organization.ROOT_ID,
}
ticket = ApplyLoginTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.open_by_system(assignees)
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(self.user)
return ticket

View File

@@ -85,18 +85,19 @@ 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 ApplyLoginAssetTicket
title = _('Login asset confirm') + ' ({})'.format(user)
from tickets.models import Ticket
data = {
'title': title,
'title': _('Login asset confirm') + ' ({})'.format(user),
'type': TicketType.login_asset_confirm,
'applicant': user,
'apply_login_user': user,
'apply_login_asset': asset,
'apply_login_system_user': system_user,
'meta': {
'apply_login_user': str(user),
'apply_login_asset': str(asset),
'apply_login_system_user': str(system_user),
},
'org_id': org_id,
}
ticket = ApplyLoginAssetTicket.objects.create(**data)
ticket.open_by_system(assignees)
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(assignees)
ticket.open(applicant=user)
return ticket

View File

@@ -2,16 +2,14 @@
#
from django_filters import rest_framework as filters
from django.db.models import Q
from django.db.models import F, 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
@@ -56,9 +54,9 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
perm_model = SystemUser
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
permission_classes = [RBACPermission, NeedMFAVerify]
http_method_names = ['get', 'options']
rbac_perms = {
'retrieve': 'applications.view_applicationaccountsecret',

View File

@@ -1,5 +1,6 @@
# 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

@@ -83,9 +83,3 @@ class AppType(models.TextChoices):
if AppCategory.is_xpack(category):
return True
return tp in ['oracle', 'postgresql', 'sqlserver']
class OracleVersion(models.TextChoices):
version_11g = '11g', '11g'
version_12c = '12c', '12c'
version_other = 'other', _('Other')

View File

@@ -11,4 +11,5 @@
"""
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.db.fields
import common.fields.model
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.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('params', common.fields.model.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.db.fields
import common.fields.model
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.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')),
('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')),
('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.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')),
('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')),
('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

@@ -1,22 +0,0 @@
# 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

@@ -1,23 +0,0 @@
# Generated by Django 3.2.12 on 2022-07-14 02:46
from django.db import migrations
def migrate_db_oracle_version_to_attrs(apps, schema_editor):
db_alias = schema_editor.connection.alias
model = apps.get_model("applications", "Application")
oracles = list(model.objects.using(db_alias).filter(type='oracle'))
for o in oracles:
o.attrs['version'] = '12c'
model.objects.using(db_alias).bulk_update(oracles, ['attrs'])
class Migration(migrations.Migration):
dependencies = [
('applications', '0021_auto_20220629_1826'),
]
operations = [
migrations.RunPython(migrate_db_oracle_version_to_attrs)
]

View File

@@ -1,48 +0,0 @@
# Generated by Django 3.1.14 on 2022-07-15 07:56
import time
from collections import defaultdict
from django.db import migrations
def migrate_account_dirty_data(apps, schema_editor):
db_alias = schema_editor.connection.alias
account_model = apps.get_model('applications', 'Account')
count = 0
bulk_size = 1000
while True:
accounts = account_model.objects.using(db_alias) \
.filter(org_id='')[count:count + bulk_size]
if not accounts:
break
accounts = list(accounts)
start = time.time()
for i in accounts:
if i.app:
org_id = i.app.org_id
elif i.systemuser:
org_id = i.systemuser.org_id
else:
org_id = ''
if org_id:
i.org_id = org_id
account_model.objects.bulk_update(accounts, ['org_id', ])
print("Update account org is empty: {}-{} using: {:.2f}s".format(
count, count + len(accounts), time.time() - start
))
count += len(accounts)
class Migration(migrations.Migration):
dependencies = [
('applications', '0022_auto_20220714_1046'),
]
operations = [
migrations.RunPython(migrate_account_dirty_data),
]

View File

@@ -10,7 +10,6 @@ from common.mixins import CommonModelMixin
from common.tree import TreeNode
from common.utils import is_uuid
from assets.models import Asset, SystemUser
from ..const import OracleVersion
from ..utils import KubernetesTree
from .. import const
@@ -215,8 +214,6 @@ class ApplicationTreeNodeMixin:
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
APP_TYPE = const.AppType
name = models.CharField(max_length=128, verbose_name=_('Name'))
category = models.CharField(
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
@@ -258,9 +255,6 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_db(self):
return self.category == const.AppCategory.db.value
def is_type(self, tp):
return self.type == tp
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:
@@ -304,15 +298,6 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
target_ip = self.attrs.get('host')
return target_ip
def get_target_protocol_for_oracle(self):
""" Oracle 类型需要单独处理,因为要携带版本号 """
if not self.is_type(self.APP_TYPE.oracle):
return
version = self.attrs.get('version', OracleVersion.version_12c)
if version == OracleVersion.version_other:
return
return 'oracle_%s' % version
class ApplicationUser(SystemUser):
class Meta:

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, SecretReadableMixin
from common.drf.serializers import MethodSerializer
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(SecretReadableMixin, AppAccountSerializer):
class AppAccountSecretSerializer(AppAccountSerializer):
class Meta(AppAccountSerializer.Meta):
fields_backup = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',

View File

@@ -31,7 +31,7 @@ class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
class RemoteAppSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField(label=_('Asset Info'))
asset_info = serializers.SerializerMethodField()
asset = ExistAssetPrimaryKeyRelatedField(
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
)

View File

@@ -1,7 +1,6 @@
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']
@@ -14,21 +13,19 @@ 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 = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Chrome password'), allow_null=True
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'),
allow_null=True
)
class ChromeSecretSerializer(ChromeSerializer):
chrome_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Chrome password'), allow_null=True, write_only=False
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'),
allow_null=True
)

View File

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

View File

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

View File

@@ -2,15 +2,9 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
from applications.const import OracleVersion
__all__ = ['OracleSerializer']
class OracleSerializer(DBSerializer):
version = serializers.ChoiceField(
choices=OracleVersion.choices, default=OracleVersion.version_12c,
allow_null=True, label=_('Version'),
help_text=_('Magnus currently supports only 11g and 12c connections')
)
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)

View File

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

View File

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

View File

@@ -1,50 +0,0 @@
from django.db.models import F
from assets.api.accounts import (
AccountFilterSet, AccountViewSet, AccountSecretsViewSet
)
from common.mixins import RecordViewLogMixin
from .. import serializers
from ..models import AuthBook
__all__ = ['AccountHistoryViewSet', 'AccountHistorySecretsViewSet']
class AccountHistoryFilterSet(AccountFilterSet):
class Meta:
model = AuthBook.history.model
fields = AccountFilterSet.Meta.fields
class AccountHistoryViewSet(AccountViewSet):
model = AuthBook.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']
def get_queryset(self):
queryset = self.model.objects.all() \
.annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname')) \
.annotate(platform=F('asset__platform__name')) \
.annotate(protocols=F('asset__protocols'))
return queryset
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 Q
from django.db.models import F, Q
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from rest_framework.decorators import action
@@ -8,14 +8,12 @@ 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.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from common.permissions import NeedMFAVerify
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
from .. import serializers
__all__ = ['AccountFilterSet', 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
class AccountFilterSet(BaseFilterSet):
@@ -81,7 +79,7 @@ class AccountViewSet(OrgBulkModelViewSet):
return Response(data={'task': task.id})
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
class AccountSecretsViewSet(AccountViewSet):
"""
因为可能要导出所有账号,所以单独建立了一个 viewset
"""
@@ -89,7 +87,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
'default': serializers.AccountSecretSerializer
}
http_method_names = ['get']
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
permission_classes = [RBACPermission, NeedMFAVerify]
rbac_perms = {
'list': 'assets.view_assetaccountsecret',
'retrieve': 'assets.view_assetaccountsecret',

View File

@@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404
from django.db.models import Q
from common.utils import get_logger, get_object_or_none
from common.mixins.api import SuggestionMixin, RenderToJsonMixin
from common.mixins.api import SuggestionMixin
from users.models import User, UserGroup
from users.serializers import UserSerializer, UserGroupSerializer
from users.filters import UserFilter
@@ -88,7 +88,7 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
return asset.platform
class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']

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_step.ticket_assignees.all()
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@@ -24,7 +24,7 @@ class SerializeToTreeNodeMixin:
'title': _name(node),
'pId': node.parent_key,
'isParent': True,
'open': True,
'open': node.is_org_root(),
'meta': {
'data': {
"id": node.id,

View File

@@ -43,7 +43,7 @@ __all__ = [
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
search_fields = ('full_value',)
search_fields = ('value',)
serializer_class = serializers.NodeSerializer
rbac_perms = {
'match': 'assets.match_node',
@@ -101,8 +101,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
class NodeChildrenApi(generics.ListCreateAPIView):
serializer_class = serializers.NodeSerializer
search_fields = ('value',)
instance = None
is_initial = False
@@ -181,15 +179,8 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
"""
model = Node
def filter_queryset(self, queryset):
if not self.request.GET.get('search'):
return queryset
queryset = super().filter_queryset(queryset)
queryset = self.model.get_ancestor_queryset(queryset)
return queryset
def list(self, request, *args, **kwargs):
nodes = self.filter_queryset(self.get_queryset()).order_by('value')
nodes = self.get_queryset().order_by('value')
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
assets = self.get_assets()
data = [*nodes, *assets]

View File

@@ -4,6 +4,7 @@ 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
@@ -101,17 +102,27 @@ 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 = serializer.validated_data
asset_or_app_id = data.get('instance_id')
data = self.decrypt_data_if_need(serializer.validated_data)
instance_id = data.get('instance_id')
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
instance.set_temp_auth(instance_id, self.request.user.id, data)
return Response(serializer.data, status=201)

View File

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

@@ -3,19 +3,6 @@
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 = [
@@ -28,5 +15,4 @@ class Migration(migrations.Migration):
name='number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
),
migrations.RunPython(create_internal_platform)
]

View File

@@ -1,26 +0,0 @@
# 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

@@ -11,7 +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.fields.model import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager

View File

@@ -29,9 +29,7 @@ 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')),
('view_assethistoryaccount', _('Can view asset history account')),
('view_assethistoryaccountsecret', _('Can view asset history account secret')),
('change_assetaccountsecret', _('Can change asset account secret'))
]
def __init__(self, *args, **kwargs):

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.db import fields
from common import fields
from orgs.mixins.models import OrgModelMixin

View File

@@ -125,9 +125,6 @@ class CommandFilterRule(OrgModelMixin):
regex.append(cmd)
continue
if not cmd:
continue
# 如果是单个字符
if cmd[-1].isalpha():
regex.append(r'\b{0}\b'.format(cmd))
@@ -168,23 +165,24 @@ 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 ApplyCommandTicket
from tickets.models import Ticket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
'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),
'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)
},
'org_id': org_id,
}
ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.open_by_system(assignees)
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(applicant=session.user_obj)
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, lazyproperty
from common.utils import get_logger
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()
@lazyproperty
@property
def gateways(self):
return self.gateway_set.filter(is_active=True)
@@ -44,9 +44,8 @@ class Domain(OrgModelMixin):
gateways = [gw for gw in self.gateways if gw.is_connective]
if gateways:
return random.choice(gateways)
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
if self.gateways:
else:
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
return random.choice(self.gateways)

View File

@@ -25,6 +25,7 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
from orgs.models import Organization
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
logger = get_logger(__name__)
@@ -97,14 +98,6 @@ class FamilyMixin:
q |= Q(key=self.key)
return Node.objects.filter(q)
@classmethod
def get_ancestor_queryset(cls, queryset, with_self=True):
parent_keys = set()
for i in queryset:
parent_keys.update(set(i.get_ancestor_keys(with_self=with_self)))
queryset = queryset.model.objects.filter(key__in=list(parent_keys)).distinct()
return queryset
@property
def children(self):
return self.get_children(with_self=False)
@@ -403,7 +396,7 @@ class NodeAllAssetsMappingMixin:
mapping[ancestor_key].update(asset_ids)
t3 = time.time()
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2 - t1, t3 - t2))
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
return mapping

View File

@@ -133,15 +133,6 @@ class AuthMixin:
self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
# 清除认证信息
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
# Remote app
from applications.models import Application
app = get_object_or_none(Application, pk=app_id)
if app and app.category_remote_app:
@@ -150,6 +141,11 @@ class AuthMixin:
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

View File

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

View File

@@ -5,8 +5,8 @@ from assets.models import AuthBook
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):
@@ -31,6 +31,10 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
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')}
@@ -53,15 +57,7 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return attrs
def get_protocols(self, v):
""" protocols 是 queryset 中返回的Post 创建成功后返回序列化时没有这个字段 """
if hasattr(v, 'protocols'):
protocols = v.protocols
elif hasattr(v, 'asset') and v.asset:
protocols = v.asset.protocols
else:
protocols = ''
protocols = protocols.replace(' ', ', ')
return protocols
return v.protocols.replace(' ', ', ')
@classmethod
def setup_eager_loading(cls, queryset):
@@ -74,7 +70,7 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return super().to_representation(instance)
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountSecretSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',

View File

@@ -1,38 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from assets.models import AuthBook
from common.drf.serializers import SecretReadableMixin
from .account import AccountSerializer, AccountSecretSerializer
class AccountHistorySerializer(AccountSerializer):
systemuser_display = serializers.SerializerMethodField(label=_('System user display'))
class Meta:
model = AuthBook.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'
@staticmethod
def get_systemuser_display(instance):
if not instance.systemuser:
return ''
return str(instance.systemuser)
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

@@ -189,9 +189,6 @@ class PlatformSerializer(serializers.ModelSerializer):
'id', 'name', 'base', 'charset',
'internal', 'meta', 'comment'
]
extra_kwargs = {
'internal': {'read_only': True},
}
class AssetSimpleSerializer(serializers.ModelSerializer):

View File

@@ -6,14 +6,12 @@ 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 = 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'))
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)
def gen_keys(self, private_key=None, password=None):
if private_key is None:
@@ -33,13 +31,6 @@ 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

@@ -5,7 +5,6 @@ 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
@@ -68,7 +67,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
}
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
class GatewayWithAuthSerializer(GatewaySerializer):
class Meta(GatewaySerializer.Meta):
extra_kwargs = {
'password': {'write_only': False},

View File

@@ -4,12 +4,10 @@ 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 ..models import SystemUser, Asset
from .utils import validate_password_for_ansible
from .utils import validate_password_contains_left_double_curly_bracket
from .base import AuthSerializerMixin
__all__ = [
@@ -25,17 +23,9 @@ 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')
)
@@ -56,9 +46,15 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
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')},
@@ -252,7 +248,7 @@ class MiniSystemUserSerializer(serializers.ModelSerializer):
fields = SystemUserSerializer.Meta.fields_mini
class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer):
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key']
@@ -268,9 +264,6 @@ class SystemUserWithAuthInfoSerializer(SecretReadableMixin, 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}
}

View File

@@ -2,16 +2,8 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """
def validate_password_contains_left_double_curly_bracket(password):
# 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

@@ -1,6 +1,6 @@
import os
import time
from openpyxl import Workbook
import pandas as pd
from collections import defaultdict, OrderedDict
from django.conf import settings
@@ -48,7 +48,7 @@ class BaseAccountHandler:
_fields = cls.get_header_fields(v)
header_fields.update(_fields)
else:
header_fields[field] = str(v.label)
header_fields[field] = 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] = str(data[field])
row_dict[header_name] = data[field]
return row_dict
@@ -72,24 +72,24 @@ class AssetAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_data_map(cls):
data_map = defaultdict(list)
def create_df(cls):
df_dict = defaultdict(list)
sheet_name = AuthBook._meta.verbose_name
accounts = AuthBook.get_queryset().select_related('systemuser')
if not accounts.first():
return data_map
return df_dict
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
for account in accounts:
account.load_auth()
row = cls.create_row(account, AccountSecretSerializer, header_fields)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return data_map
return df_dict
class AppAccountHandler(BaseAccountHandler):
@@ -101,19 +101,19 @@ class AppAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_data_map(cls):
data_map = defaultdict(list)
def create_df(cls):
df_dict = 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)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
return data_map
return df_dict
handler_map = {
@@ -142,18 +142,15 @@ class AccountBackupHandler:
if not handler:
continue
data_map = handler.create_data_map()
if not data_map:
df_dict = handler.create_df()
if not df_dict:
continue
filename = handler.get_filename(self.plan_name)
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)
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)
files.append(filename)
timedelta = round((time.time() - time_start), 2)
logger.info('步骤完成: 用时 {}s'.format(timedelta))

View File

@@ -32,18 +32,17 @@ 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, **kwargs):
algorithm = kwargs.get('algorithm')
def get_push_unixlike_system_user_tasks(system_user, username=None):
comment = system_user.name
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
@@ -105,7 +104,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
'module': 'user',
'args': 'name={} shell={} state=present password={}'.format(
username, system_user.shell,
encrypt_password(password, salt="K3mIlKK", algorithm=algorithm),
encrypt_password(password, salt="K3mIlKK"),
),
}
})
@@ -139,7 +138,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
return tasks
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs):
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
if username is None:
username = system_user.username
password = system_user.password
@@ -177,7 +176,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, algorithm=None):
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
"""
获取推送系统用户的 ansible 命令,跟资产无关
:param system_user:
@@ -191,16 +190,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, algorithm=algorithm)
return get_tasks(system_user)
tasks = []
# 仅推送这个username
if username is not None:
tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
tasks.extend(get_tasks(system_user, username))
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, algorithm=algorithm))
tasks.extend(get_tasks(system_user, _username))
return tasks
@@ -245,11 +244,7 @@ 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)
algorithm = 'des' if a.platform.name == 'AIX' else 'sha512'
tasks = get_push_system_user_tasks(
system_user, platform, username=u,
algorithm=algorithm
)
tasks = get_push_system_user_tasks(system_user, platform, username=u)
run_task(tasks, [a])
@@ -274,7 +269,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 or system_user.username, asset
system_user.name, username, asset
)
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)

View File

@@ -13,8 +13,6 @@ router = BulkRouter()
router.register(r'assets', api.AssetViewSet, 'asset')
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

@@ -12,7 +12,6 @@ from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
from orgs.utils import current_org
from ops.models import CommandExecution
from . import filters
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
@@ -127,7 +126,9 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
serializer_class = CommandExecutionHostsRelationSerializer
m2m_field = CommandExecution.hosts.field
filterset_class = filters.CommandExecutionFilter
filterset_fields = [
'id', 'asset', 'commandexecution'
]
search_fields = ('asset__hostname', )
http_method_names = ['options', 'get']
rbac_perms = {

View File

@@ -3,23 +3,3 @@
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', 'AuthBook',
# applications
'Application',
# orgs
'Organization',
# settings
'Setting',
# perms
'AssetPermission', 'ApplicationPermission',
# xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
)

View File

@@ -1,14 +1,10 @@
from django.db.models import F, Value
from django.db.models.functions import Concat
from django_filters.rest_framework import CharFilter
from rest_framework import filters
from rest_framework.compat import coreapi, coreschema
from orgs.utils import current_org
from ops.models import CommandExecution
from common.drf.filters import BaseFilterSet
__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter']
__all__ = ['CurrentOrgMembersFilter']
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
@@ -34,22 +30,3 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
else:
queryset = queryset.filter(user__in=self._get_user_list())
return queryset
class CommandExecutionFilter(BaseFilterSet):
hostname_ip = CharFilter(method='filter_hostname_ip')
class Meta:
model = CommandExecution.hosts.through
fields = (
'id', 'asset', 'commandexecution', 'hostname_ip'
)
def filter_hostname_ip(self, queryset, name, value):
queryset = queryset.annotate(
hostname_ip=Concat(
F('asset__hostname'), Value('('),
F('asset__ip'), Value(')')
)
).filter(hostname_ip__icontains=value)
return queryset

View File

@@ -1,18 +0,0 @@
# 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,12 +49,10 @@ 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,7 +1,5 @@
# -*- coding: utf-8 -*-
#
import time
from django.db.models.signals import (
post_save, m2m_changed, pre_delete
)
@@ -23,7 +21,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, create_operate_log
from .utils import write_login_log
from . import models, serializers
from .models import OperateLog
from orgs.utils import current_org
@@ -38,6 +36,26 @@ 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
@@ -51,7 +69,6 @@ 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
@@ -63,6 +80,28 @@ 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'),
@@ -277,7 +316,6 @@ 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

@@ -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)
FTPLog.objects.filter(date_start__lt=expired_day).delete()
FTPLog.objects.filter(datetime__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)

View File

@@ -1,17 +1,9 @@
import csv
import codecs
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__)
from .const import DEFAULT_CITY
from common.utils import validate_ip, get_ip_city
def get_excel_response(filename):
@@ -44,25 +36,3 @@ 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,7 +5,6 @@ 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 *

View File

@@ -1,64 +0,0 @@
# -*- 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,64 +1,58 @@
import abc
import os
import json
import base64
# -*- coding: utf-8 -*-
#
import urllib.parse
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework.exceptions import PermissionDenied
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from rest_framework.request import Request
import json
from typing import Callable
import os
import base64
import ctypes
from common.drf.api import JMSModelViewSet
from common.http import is_true
from django.conf import settings
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.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 rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
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.http import is_true
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,
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
)
from ..models import ConnectionToken
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin']
class ConnectionTokenMixin:
class ClientProtocolMixin:
"""
下载客户端支持的连接文件,里面包含了 token和 其他连接信息
- [x] RDP
- [ ] KoKo
本质上,这里还是暴露出 token 来,进行使用
"""
request: Request
@staticmethod
def check_token_valid(token: ConnectionToken):
is_valid, error = token.check_valid()
if not is_valid:
raise PermissionDenied(error)
@abc.abstractmethod
def get_request_resource_user(self, serializer):
raise NotImplementedError
def get_request_resources(self, serializer):
user = self.get_request_resource_user(serializer)
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data.get('system_user')
return user, asset, application, system_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)
get_serializer: Callable
create_token: Callable
get_serializer_context: Callable
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
@@ -70,32 +64,22 @@ class ConnectionTokenMixin:
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
return endpoint
def get_request_resource(self, serializer):
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data['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 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_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 = {
def get_rdp_file_content(self, serializer):
options = {
'full address:s': '',
'username:s': '',
# 'screen mode id:i': '1',
@@ -121,216 +105,424 @@ class ConnectionTokenMixin:
'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': '',
}
# 设置磁盘挂载
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
# 设置宽高
asset, application, system_user, user = self.get_request_resource(serializer)
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 地址
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=asset, application=application
)
options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 用户名
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
options['domain:s'] = system_user.ad_domain
# 宽高
if width and height:
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
# 设置其他选项
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')
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
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
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)
else:
name = '*'
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
content = ''
for k, v in rdp_options.items():
for k, v in options.items():
content += f'{k}:{v}\n'
return name, content
return filename, content
@staticmethod
def get_connect_filename(prefix_name):
prefix_name = prefix_name.replace('/', '_')
prefix_name = prefix_name.replace('\\', '_')
prefix_name = prefix_name.replace('.', '_')
filename = f'{prefix_name}-jumpserver'
filename = urllib.parse.quote(filename)
return filename
def get_ssh_token(self, token: ConnectionToken):
if token.asset:
name = token.asset.hostname
elif token.application:
name = token.application.name
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
else:
name = '*'
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=token.asset, application=token.application
protocol='ssh', asset=asset, application=application
)
data = {
content = {
'ip': endpoint.host,
'port': str(endpoint.ssh_port),
'username': 'JMS-{}'.format(str(token.id)),
'password': token.secret
'username': f'JMS-{token}',
'password': secret
}
token = json.dumps(data)
return filename, token
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 ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
'type', 'user_display', 'system_user_display',
'application_display', 'asset_display'
)
search_fields = filterset_fields
class SecretDetailMixin:
valid_token: Callable
request: Request
get_serializer: Callable
@staticmethod
def _get_application_secret_detail(application):
gateway = None
remote_app = None
asset = None
if application.category_remote_app:
remote_app = application.get_rdp_remote_app_setting()
asset = application.get_remote_app_asset()
domain = asset.domain
else:
domain = application.domain
if domain and domain.has_gateway():
gateway = domain.random_gateway()
return {
'asset': asset,
'application': application,
'gateway': gateway,
'domain': domain,
'remote_app': remote_app,
}
@staticmethod
def _get_asset_secret_detail(asset):
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
'domain': asset.domain,
'gateway': gateway,
'remote_app': None,
}
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
perm_required = 'authentication.view_connectiontokensecret'
# 非常重要的 api再逻辑层再判断一下双重保险
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token = request.data.get('token', '')
try:
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
except serializers.ValidationError as e:
post_auth_failed.send(
sender=self.__class__, username='', request=self.request,
reason=_('Invalid token')
)
raise e
data = dict(
id=token, secret=value.get('secret', ''),
user=user, system_user=system_user,
expired_at=expired_at, actions=actions
)
cmd_filter_kwargs = {
'system_user_id': system_user.id,
'user_id': user.id,
}
if asset:
asset_detail = self._get_asset_secret_detail(asset)
system_user.load_asset_more_auth(asset.id, user.username, user.id)
data['type'] = 'asset'
data.update(asset_detail)
cmd_filter_kwargs['asset_id'] = asset.id
else:
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.username, user.id)
data['type'] = 'application'
data.update(app_detail)
cmd_filter_kwargs['application_id'] = app.id
from assets.models import CommandFilterRule
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
data['cmd_filter_rules'] = cmd_filter_rules
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
class TokenCacheMixin:
""" endpoint smart view 用到此类来解析token中的资产、应用 """
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
}
return data
class UserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
SecretDetailMixin, TokenCacheMixin, GenericViewSet
):
serializer_classes = {
'default': ConnectionTokenSerializer,
'list': ConnectionTokenDisplaySerializer,
'retrieve': ConnectionTokenDisplaySerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'retrieve': 'authentication.view_connectiontoken',
'GET': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'expire': 'authentication.add_connectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user)
@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
def get_request_resource_user(self, serializer):
return self.request.user
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
def get_object(self):
if self.request.user.is_service_account:
# TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中
obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk'))
else:
obj = super(ConnectionTokenViewSet, self).get_object()
return obj
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
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 = {
'url': 'jms://{}'.format(protocol_data)
}
return Response(data=data)
@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)
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
serializer_classes = {
'default': SuperConnectionTokenSerializer,
}
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken'
}
def get_request_resource_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['PATCH'], detail=False)
@action(methods=[PATCH], detail=False)
def renewal(self, request, *args, **kwargs):
from common.utils.timezone import as_current_tz
""" 续期 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)
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}'
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())
}
return Response(data=data, status=status.HTTP_200_OK)
if asset:
value.update({
'type': 'asset',
'asset': str(asset.id),
'hostname': asset.hostname,
})
elif application:
value.update({
'type': 'application',
'application': application.id,
'application_name': str(application)
})
self.set_token_to_cache(token, value, ttl)
return token, secret
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
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'
data = {
"id": token, 'secret': secret,
'type': tp, 'protocol': system_user.protocol,
'expire_time': self.get_token_ttl(token),
}
return Response(data, status=201)
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
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")
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)
if not has_perm:
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app, expired_at, actions
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,11 +2,10 @@ 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__)
@@ -27,8 +26,9 @@ class DingTalkQRUnBindBase(APIView):
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
permission_classes = (IsAuthPasswdTimeValid,)
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
user_id_url_kwarg = 'user_id'

View File

@@ -2,11 +2,10 @@ 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__)
@@ -27,7 +26,7 @@ class FeiShuQRUnBindBase(APIView):
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
permission_classes = (IsAuthPasswdTimeValid,)
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):

View File

@@ -6,8 +6,6 @@ from rest_framework.permissions import AllowAny
from common.utils import get_logger
from .. import errors, mixins
from django.contrib.auth import logout as auth_logout
__all__ = ['TicketStatusApi']
logger = get_logger(__name__)
@@ -19,15 +17,7 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
def get(self, request, *args, **kwargs):
try:
self.check_user_login_confirm()
self.request.session['auth_third_party_done'] = 1
return Response({"msg": "ok"})
except errors.LoginConfirmOtherError as e:
reason = e.msg
username = e.username
self.send_auth_signal(success=False, username=username, reason=reason)
# 若为三方登录,此时应退出登录
auth_logout(request)
return Response(e.as_data(), status=200)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
@@ -35,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket()
if ticket:
request.session.pop('auth_ticket_id', '')
ticket.close()
ticket.close(processor=self.get_user_from_session())
return Response('', status=200)

View File

@@ -10,17 +10,22 @@ 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', 'MFASendCodeApi'
'MFAChallengeVerifyApi', 'UserOtpVerifyApi',
'MFASendCodeApi'
]
@@ -83,3 +88,30 @@ 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

@@ -27,10 +27,8 @@ 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.get_user_or_auth(serializer.validated_data)
user = self.check_user_auth_if_need()
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,11 +2,10 @@ 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__)
@@ -27,8 +26,9 @@ class WeComQRUnBindBase(APIView):
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
permission_classes = (IsAuthPasswdTimeValid,)
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
user_id_url_kwarg = 'user_id'

View File

@@ -49,7 +49,7 @@ class JMSBaseAuthBackend:
if not allow:
info = 'User {} skip authentication backend {}, because it not in {}'
info = info.format(username, backend_name, ','.join(allowed_backend_names))
logger.info(info)
logger.debug(info)
return allow

View File

@@ -3,10 +3,9 @@
from django.urls import path
import django_cas_ng.views
from .views import CASLoginView
urlpatterns = [
path('login/', CASLoginView.as_view(), name='cas-login'),
path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
]

View File

@@ -1,15 +0,0 @@
from django_cas_ng.views import LoginView
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
__all__ = ['LoginView']
class CASLoginView(LoginView):
def get(self, request):
try:
return super().get(request)
except PermissionDenied:
return HttpResponseRedirect('/')

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, exceptions.ValidationError):
except AccessKey.DoesNotExist:
return None, None

View File

@@ -157,8 +157,6 @@ 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

@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
#
from .backends import *

View File

@@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
#
import requests
from django.contrib.auth import get_user_model
from django.utils.http import urlencode
from django.conf import settings
from django.urls import reverse
from common.utils import get_logger
from users.utils import construct_user_email
from authentication.utils import build_absolute_uri
from common.exceptions import JMSException
from .signals import (
oauth2_create_or_update_user, oauth2_user_login_failed,
oauth2_user_login_success
)
from ..base import JMSModelBackend
__all__ = ['OAuth2Backend']
logger = get_logger(__name__)
class OAuth2Backend(JMSModelBackend):
@staticmethod
def is_enabled():
return settings.AUTH_OAUTH2
def get_or_create_user_from_userinfo(self, request, userinfo):
log_prompt = "Get or Create user [OAuth2Backend]: {}"
logger.debug(log_prompt.format('start'))
# Construct user attrs value
user_attrs = {}
for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items():
user_attrs[field] = userinfo.get(attr, '')
username = user_attrs.get('username')
if not username:
error_msg = 'username is missing'
logger.error(log_prompt.format(error_msg))
raise JMSException(error_msg)
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))
user, created = get_user_model().objects.get_or_create(
username=username, defaults=user_attrs
)
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => oauth2 create or update user"))
oauth2_create_or_update_user.send(
sender=self.__class__, request=request, user=user, created=created,
attrs=user_attrs
)
return user, created
@staticmethod
def get_response_data(response_data):
if response_data.get('data') is not None:
response_data = response_data['data']
return response_data
@staticmethod
def get_query_dict(response_data, query_dict):
query_dict.update({
'uid': response_data.get('uid', ''),
'access_token': response_data.get('access_token', '')
})
return query_dict
def authenticate(self, request, code=None, **kwargs):
log_prompt = "Process authenticate [OAuth2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if code is None:
logger.error(log_prompt.format('code is missing'))
return None
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
access_token_url = '{url}?{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict)
)
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
requests_func = getattr(requests, token_method, requests.get)
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
headers = {
'Accept': 'application/json'
}
access_token_response = requests_func(access_token_url, headers=headers)
try:
access_token_response.raise_for_status()
access_token_response_data = access_token_response.json()
response_data = self.get_response_data(access_token_response_data)
except Exception as e:
error = "Json access token response error, access token response " \
"content is: {}, error is: {}".format(access_token_response.content, str(e))
logger.error(log_prompt.format(error))
return None
query_dict = self.get_query_dict(response_data, query_dict)
headers = {
'Accept': 'application/json',
'Authorization': 'token {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
userinfo_url = '{url}?{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT,
query=urlencode(query_dict)
)
userinfo_response = requests.get(userinfo_url, headers=headers)
try:
userinfo_response.raise_for_status()
userinfo_response_data = userinfo_response.json()
if 'data' in userinfo_response_data:
userinfo = userinfo_response_data['data']
else:
userinfo = userinfo_response_data
except Exception as e:
error = "Json userinfo response error, userinfo response " \
"content is: {}, error is: {}".format(userinfo_response.content, str(e))
logger.error(log_prompt.format(error))
return None
try:
logger.debug(log_prompt.format('Update or create oauth2 user'))
user, created = self.get_or_create_user_from_userinfo(request, userinfo)
except JMSException:
return None
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OAuth2 user login success'))
logger.debug(log_prompt.format('Send signal => oauth2 user login success'))
oauth2_user_login_success.send(sender=self.__class__, request=request, user=user)
return user
else:
logger.debug(log_prompt.format('OAuth2 user login failed'))
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
oauth2_user_login_failed.send(
sender=self.__class__, request=request, username=user.username,
reason=_('User invalid, disabled or expired')
)
return None

View File

@@ -1,9 +0,0 @@
from django.dispatch import Signal
oauth2_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)
oauth2_user_login_success = Signal(providing_args=['request', 'user'])
oauth2_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])

View File

@@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback')
]

View File

@@ -1,58 +0,0 @@
from django.views import View
from django.conf import settings
from django.contrib.auth import login
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
from authentication.utils import build_absolute_uri
from common.utils import get_logger
from authentication.mixins import authenticate
logger = get_logger(__file__)
class OAuth2AuthRequestView(View):
def get(self, request):
log_prompt = "Process OAuth2 GET requests: {}"
logger.debug(log_prompt.format('Start'))
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
'scope': settings.AUTH_OAUTH2_SCOPE,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
redirect_url = '{url}?{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
query=urlencode(query_dict)
)
logger.debug(log_prompt.format('Redirect login url'))
return HttpResponseRedirect(redirect_url)
class OAuth2AuthCallbackView(View):
http_method_names = ['get', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
logger.debug(log_prompt.format('Start'))
callback_params = request.GET
if 'code' in callback_params:
logger.debug(log_prompt.format('Process authenticate'))
user = authenticate(code=callback_params['code'], request=request)
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
login(self.request, user)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI)

View File

@@ -9,7 +9,6 @@
import base64
import requests
from rest_framework.exceptions import ParseError
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
@@ -19,11 +18,9 @@ from django.urls import reverse
from django.conf import settings
from common.utils import get_logger
from authentication.utils import build_absolute_uri_for_oidc
from users.utils import construct_user_email
from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token
from .utils import validate_and_return_id_token, build_absolute_uri
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
@@ -42,22 +39,17 @@ class UserMixin:
logger.debug(log_prompt.format('start'))
sub = claims['sub']
# 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')
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)
)
)
user, created = get_user_model().objects.get_or_create(
username=username, defaults=user_attrs
username=username, defaults={"name": name, "email": email}
)
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => openid create or update user"))
@@ -111,44 +103,21 @@ 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_for_oidc(
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
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())
}
# 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'))
@@ -289,11 +258,6 @@ 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))
@@ -322,3 +286,5 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid"
)
return None

View File

@@ -8,7 +8,7 @@
import datetime as dt
from calendar import timegm
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_bytes, smart_bytes
@@ -110,3 +110,17 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
raise SuspiciousOperation('Incorrect id_token: nonce')
logger.debug(log_prompt.format('End'))
def build_absolute_uri(request, path=None):
"""
Build absolute redirect uri
"""
if path is None:
path = '/'
if settings.BASE_SITE_URL:
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
else:
redirect_uri = request.build_absolute_uri(path)
return redirect_uri

View File

@@ -20,8 +20,7 @@ from django.utils.crypto import get_random_string
from django.utils.http import is_safe_url, urlencode
from django.views.generic import View
from authentication.utils import build_absolute_uri_for_oidc
from .utils import get_logger
from .utils import get_logger, build_absolute_uri
logger = get_logger(__file__)
@@ -51,7 +50,7 @@ class OIDCAuthRequestView(View):
'scope': settings.AUTH_OPENID_SCOPES,
'response_type': 'code',
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'redirect_uri': build_absolute_uri_for_oidc(
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
})
@@ -217,7 +216,7 @@ class OIDCEndSessionView(View):
""" Returns the end-session URL. """
q = QueryDict(mutable=True)
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
self.request.session['oidc_auth_id_token']
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())

View File

@@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend):
return user, created
def authenticate(self, request, saml_user_data=None, **kwargs):
log_prompt = "Process authenticate [SAML2Backend]: {}"
log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
logger.debug(log_prompt.format('Start'))
if saml_user_data is None:
logger.error(log_prompt.format('saml_user_data is missing'))
@@ -48,7 +48,7 @@ class SAML2Backend(JMSModelBackend):
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
username = saml_user_data.get('username')
if not username:
logger.warning(log_prompt.format('username is missing'))
logger.debug(log_prompt.format('username is missing'))
return None
user, created = self.get_or_create_from_saml_data(request, **saml_user_data)

View File

@@ -74,37 +74,27 @@ class PrepareRequestMixin:
return idp_settings
@staticmethod
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
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
}
}
}
return request_attribute_template
return request_attribute_template
else:
return {}
@staticmethod
def get_advanced_settings():
@@ -177,14 +167,11 @@ 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

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

View File

@@ -1,30 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,30 +0,0 @@
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,37 +1,2 @@
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

@@ -0,0 +1,367 @@
# -*- 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

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

View File

@@ -1,67 +0,0 @@
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

@@ -1,161 +0,0 @@
# -*- 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 = ''
msg = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.msg
)
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):
if not self.msg:
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):
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)
else:
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)
# 先处理 msg 在 super记录日志时原因才准确
super().__init__(error=error, username=username, ip=ip, request=request)
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 LoginACLIPAndTimePeriodNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("Current IP and 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

@@ -1,38 +0,0 @@
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_not_bound'
default_detail = _('WeCom is already bound')
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