mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3355ab0ec | ||
|
|
81598a5264 | ||
|
|
298f6ba41d | ||
|
|
8e43e9ee2b | ||
|
|
adc8a8f7d3 | ||
|
|
1e3da50979 | ||
|
|
7ac385d64c | ||
|
|
2be74c4b84 | ||
|
|
75a72fb182 | ||
|
|
4c2274b14e | ||
|
|
a024f26768 | ||
|
|
2898c35970 | ||
|
|
62f5662bd0 | ||
|
|
0fe221019a | ||
|
|
d745314aa1 | ||
|
|
153fad9ac7 | ||
|
|
0792c7ec49 | ||
|
|
e617697553 | ||
|
|
9dc7da3595 | ||
|
|
f7f4d3a42e | ||
|
|
70fcbfe883 | ||
|
|
9e16b79abe | ||
|
|
8c839784fb | ||
|
|
10adb4e6b7 | ||
|
|
75c011f1c5 | ||
|
|
a882ca0d51 | ||
|
|
e0a2d03f44 | ||
|
|
2414f34a5a | ||
|
|
2aebfa51b2 | ||
|
|
f91bfedc50 | ||
|
|
68aad56bad | ||
|
|
556ce0a146 | ||
|
|
95f8b12912 | ||
|
|
25ae790f7d | ||
|
|
0464b1a9e6 | ||
|
|
3755f8f33a | ||
|
|
85b2ec2e6a | ||
|
|
9d1e94d3c2 | ||
|
|
be75edcb41 | ||
|
|
a5c6ba6cd6 | ||
|
|
81ef614820 | ||
|
|
c6949b4f68 | ||
|
|
a5acdb9f60 | ||
|
|
2366f02d10 | ||
|
|
dade0cadda | ||
|
|
e096244e75 | ||
|
|
3bc307d666 | ||
|
|
810c500402 | ||
|
|
6c0d0c3e92 | ||
|
|
af1150bb86 | ||
|
|
f7cbcc46f4 | ||
|
|
327c6beab4 | ||
|
|
196663f205 | ||
|
|
15423291cc | ||
|
|
021635b850 | ||
|
|
992c1407b6 | ||
|
|
1322106c91 | ||
|
|
42202bd528 | ||
|
|
b24d2f628a | ||
|
|
041302d5d2 | ||
|
|
a08dd5ee72 | ||
|
|
09ef72a4a8 | ||
|
|
26cf64ad2d | ||
|
|
0a04f0f351 | ||
|
|
1029556902 |
18
Dockerfile
18
Dockerfile
@@ -29,11 +29,12 @@ ARG TOOLS=" \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& apt update \
|
||||
&& apt update && sleep 1 && apt update \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& apt -y install ${TOOLS} \
|
||||
@@ -47,12 +48,19 @@ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
|
||||
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/ \
|
||||
&& 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/oracle/ \
|
||||
&& echo "/opt/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& cd /opt/oracle/ \
|
||||
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \
|
||||
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \
|
||||
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \
|
||||
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
|
||||
&& rm -f ${ORACLE_FILE}
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
60
README.md
60
README.md
@@ -1,10 +1,13 @@
|
||||
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
|
||||
<p align="center">
|
||||
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
||||
</p>
|
||||
<h3 align="center">多云环境下更好用的堡垒机</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
|
||||
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jumpserver/jumpserver.svg" /></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
@@ -15,7 +18,7 @@
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
@@ -28,9 +31,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多租户: 一套系统,多个子公司或部门同时使用;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
### UI 展示
|
||||
@@ -55,12 +58,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
|
||||
### 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目
|
||||
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目
|
||||
| 项目 | 状态 | 描述 |
|
||||
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer)| <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
### 社区
|
||||
|
||||
@@ -75,27 +81,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
感谢以下贡献者,让 JumpServer 更加完善
|
||||
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/koko/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/lina/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/luna/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
|
||||
</a>
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
|
||||
### 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web 页面连接使用数据库,JumpServer Web 数据库依赖
|
||||
|
||||
|
||||
### JumpServer 企业版
|
||||
@@ -103,14 +95,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851);
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516);
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732);
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708);
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
|
||||
|
||||
### 安全说明
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
|
||||
@@ -13,19 +14,21 @@ class ChromeSerializer(RemoteAppSerializer):
|
||||
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
|
||||
)
|
||||
chrome_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Target URL'), allow_null=True,
|
||||
)
|
||||
chrome_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Chrome username'), allow_null=True,
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome username'), allow_null=True,
|
||||
)
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class ChromeSecretSerializer(ChromeSerializer):
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True, write_only=False
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
|
||||
@@ -19,14 +20,14 @@ class CustomSerializer(RemoteAppSerializer):
|
||||
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Custom password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class CustomSecretSerializer(RemoteAppSerializer):
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Custom password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
|
||||
@@ -29,14 +30,14 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
||||
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Mysql workbench password'),
|
||||
allow_null=True,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Mysql workbench password'),
|
||||
allow_null=True,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
|
||||
@@ -25,14 +26,14 @@ class VMwareClientSerializer(RemoteAppSerializer):
|
||||
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Vmware password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class VMwareClientSecretSerializer(RemoteAppSerializer):
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Vmware password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ def create_internal_platform(apps, schema_editor):
|
||||
model.objects.using(db_alias).update_or_create(
|
||||
name=name, defaults=defaults
|
||||
)
|
||||
migrations.RunPython(create_internal_platform)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -133,6 +133,15 @@ 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:
|
||||
@@ -141,11 +150,6 @@ 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
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
|
||||
@@ -32,10 +31,6 @@ 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')}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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):
|
||||
@@ -33,7 +34,8 @@ class AuthSerializer(serializers.ModelSerializer):
|
||||
|
||||
class AuthSerializerMixin(serializers.ModelSerializer):
|
||||
password = EncryptedField(
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024
|
||||
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=4096
|
||||
|
||||
@@ -9,7 +9,7 @@ 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_contains_left_double_curly_bracket
|
||||
from .utils import validate_password_for_ansible
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
@@ -25,6 +25,11 @@ 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'))
|
||||
@@ -51,15 +56,9 @@ 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')},
|
||||
|
||||
@@ -2,8 +2,16 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def validate_password_contains_left_double_curly_bracket(password):
|
||||
def validate_password_for_ansible(password):
|
||||
""" 校验 Ansible 不支持的特殊字符 """
|
||||
# validate password contains left double curly bracket
|
||||
# check password not contains `{{`
|
||||
# Ansible 推送的时候不支持
|
||||
if '{{' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||
# Ansible Windows 推送的时候不支持
|
||||
if "'" in password:
|
||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||
if '"' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `"` '))
|
||||
|
||||
|
||||
@@ -33,16 +33,17 @@ def _dump_args(args: dict):
|
||||
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
|
||||
comment = system_user.name
|
||||
algorithm = kwargs.get('algorithm')
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
|
||||
comment = system_user.name
|
||||
if system_user.username_same_with_user:
|
||||
from users.models import User
|
||||
user = User.objects.filter(username=username).only('name', 'username').first()
|
||||
if user:
|
||||
comment = f'{system_user.name}[{str(user)}]'
|
||||
comment = comment.replace(' ', '')
|
||||
|
||||
password = system_user.password
|
||||
public_key = system_user.public_key
|
||||
@@ -273,7 +274,7 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
|
||||
# if username is None:
|
||||
# username = system_user.username
|
||||
task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format(
|
||||
system_user.name, username, asset
|
||||
system_user.name, username or system_user.username, asset
|
||||
)
|
||||
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_delete
|
||||
)
|
||||
@@ -274,6 +276,8 @@ 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")
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||
write_login_log(**data)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 *
|
||||
|
||||
85
apps/authentication/api/confirm.py
Normal file
85
apps/authentication/api/confirm.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.generics import ListCreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from ..mfa import MFAOtp
|
||||
from ..const import ConfirmType
|
||||
from ..mixins import authenticate
|
||||
from ..serializers import ConfirmSerializer
|
||||
|
||||
|
||||
class ConfirmViewSet(ListCreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = ConfirmSerializer
|
||||
|
||||
def check(self, confirm_type: str):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
return self.user.mfa_enabled
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
return self.user.is_password_authenticate()
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
return not self.user.is_password_authenticate()
|
||||
|
||||
def authenticate(self, confirm_type, secret_key):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
ok, msg = MFAOtp(self.user).check_code(secret_key)
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
ok = authenticate(self.request, username=self.user.username, password=secret_key)
|
||||
msg = '' if ok else _('Authentication failed password incorrect')
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
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')
|
||||
SPECIFIED_TIME = 5
|
||||
msg = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
|
||||
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, ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.request.user
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return Response('ok')
|
||||
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return Response('ok')
|
||||
|
||||
data = []
|
||||
for i, confirm_type in enumerate(ConfirmType.values, 1):
|
||||
if self.check(confirm_type):
|
||||
data.append({'name': confirm_type, 'level': i})
|
||||
msg = _('This action require verify your MFA')
|
||||
return Response({'error': msg, 'backends': data}, status=400)
|
||||
|
||||
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')
|
||||
secret_key = validated_data.get('secret_key')
|
||||
ok, msg = self.authenticate(confirm_type, secret_key)
|
||||
if ok:
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
return Response('ok')
|
||||
return Response({'error': msg}, status=400)
|
||||
@@ -18,6 +18,7 @@ from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
|
||||
from applications.models import Application
|
||||
from authentication.signals import post_auth_failed
|
||||
@@ -160,7 +161,6 @@ class ClientProtocolMixin:
|
||||
options['alternate shell:s'] = app
|
||||
options['remoteapplicationprogram:s'] = app
|
||||
options['remoteapplicationname:s'] = name
|
||||
options['remoteapplicationcmdline:s'] = '- ' + self.get_encrypt_cmdline(application)
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
@@ -361,23 +361,7 @@ 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):
|
||||
def renewal_token(self, token, ttl=None):
|
||||
value = self.get_token_from_cache(token)
|
||||
if value:
|
||||
pre_ttl = self.get_token_ttl(token)
|
||||
@@ -394,6 +378,23 @@ class TokenCacheMixin:
|
||||
}
|
||||
return data
|
||||
|
||||
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=None):
|
||||
key = self.get_token_cache_key(token)
|
||||
ttl = ttl or settings.CONNECTION_TOKEN_EXPIRATION
|
||||
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 get_token_cache_key(self, token):
|
||||
return self.CACHE_KEY_PREFIX.format(token)
|
||||
|
||||
|
||||
class BaseUserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
@@ -415,7 +416,7 @@ class BaseUserConnectionTokenViewSet(
|
||||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
|
||||
def create_token(self, user, asset, application, system_user, ttl=None):
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
secret = random_string(16)
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
@@ -26,9 +26,8 @@ class DingTalkQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
@@ -26,7 +26,7 @@ class FeiShuQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
@@ -26,9 +26,8 @@ class WeComQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
||||
@@ -198,6 +198,6 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||
return None, None
|
||||
user, secret = key.user, str(key.secret)
|
||||
return user, secret
|
||||
except AccessKey.DoesNotExist:
|
||||
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -157,6 +157,8 @@ class LDAPUser(_LDAPUser):
|
||||
|
||||
def _populate_user_from_attributes(self):
|
||||
for field, attr in self.settings.USER_ATTR_MAP.items():
|
||||
if field in ['groups']:
|
||||
continue
|
||||
try:
|
||||
value = self.attrs[attr][0]
|
||||
value = value.strip()
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
@@ -39,17 +40,22 @@ class UserMixin:
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
sub = claims['sub']
|
||||
name = claims.get('name', sub)
|
||||
username = claims.get('preferred_username', sub)
|
||||
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
|
||||
logger.debug(
|
||||
log_prompt.format(
|
||||
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
|
||||
)
|
||||
)
|
||||
|
||||
# Construct user attrs value
|
||||
user_attrs = {}
|
||||
for field, attr in settings.AUTH_OPENID_USER_ATTR_MAP.items():
|
||||
user_attrs[field] = claims.get(attr, sub)
|
||||
email = user_attrs.get('email', '')
|
||||
email = construct_user_email(user_attrs.get('username'), email)
|
||||
user_attrs.update({'email': email})
|
||||
|
||||
logger.debug(log_prompt.format(user_attrs))
|
||||
|
||||
username = user_attrs.get('username')
|
||||
name = user_attrs.get('name')
|
||||
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults={"name": name, "email": email}
|
||||
username=username, defaults=user_attrs
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => openid create or update user"))
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
from django.db.models import TextChoices
|
||||
|
||||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
||||
|
||||
|
||||
class ConfirmType(TextChoices):
|
||||
RELOGIN = 'relogin', 'Re-Login'
|
||||
PASSWORD = 'password', 'Password'
|
||||
MFA = 'mfa', 'MFA'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .token import *
|
||||
from .connect_token import *
|
||||
from .password_mfa import *
|
||||
from .confirm import *
|
||||
|
||||
11
apps/authentication/serializers/confirm.py
Normal file
11
apps/authentication/serializers/confirm.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..const import ConfirmType
|
||||
|
||||
|
||||
class ConfirmSerializer(serializers.Serializer):
|
||||
confirm_type = serializers.ChoiceField(
|
||||
required=True, choices=ConfirmType.choices
|
||||
)
|
||||
secret_key = EncryptedField()
|
||||
@@ -26,6 +26,7 @@ urlpatterns = [
|
||||
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
||||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('confirm/', api.ConfirmViewSet.as_view(), name='user-confirm'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
|
||||
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
||||
|
||||
@@ -9,8 +9,9 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.utils import is_auth_confirm_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
@@ -118,17 +119,12 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
|
||||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db.utils import IntegrityError
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
@@ -89,17 +89,12 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
||||
|
||||
|
||||
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
@@ -118,17 +118,12 @@ class WeComOAuthMixin(WeComBaseMixin, View):
|
||||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
import csv
|
||||
|
||||
import pyzipper
|
||||
import requests
|
||||
|
||||
|
||||
def create_csv_file(filename, headers, rows, ):
|
||||
@@ -18,3 +20,11 @@ def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filenames
|
||||
for encrypted_filename in encrypted_filenames:
|
||||
with open(encrypted_filename, 'rb') as f:
|
||||
zf.writestr(os.path.basename(encrypted_filename), f.read())
|
||||
|
||||
|
||||
def download_file(src, path):
|
||||
with requests.get(src, stream=True) as r:
|
||||
r.raise_for_status()
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
@@ -13,10 +13,6 @@ reader = None
|
||||
|
||||
|
||||
def get_ip_city_by_geoip(ip):
|
||||
if not ip or '.' not in ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
global reader
|
||||
if reader is None:
|
||||
path = os.path.join(os.path.dirname(__file__), 'GeoLite2-City.mmdb')
|
||||
@@ -32,7 +28,7 @@ def get_ip_city_by_geoip(ip):
|
||||
try:
|
||||
response = reader.city(ip)
|
||||
except GeoIP2Error:
|
||||
return {}
|
||||
return _("Unknown")
|
||||
|
||||
city_names = response.city.names or {}
|
||||
lang = settings.LANGUAGE_CODE[:2]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import ipdb
|
||||
|
||||
@@ -11,13 +10,13 @@ ipip_db = None
|
||||
|
||||
def get_ip_city_by_ipip(ip):
|
||||
global ipip_db
|
||||
if not ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
if ipip_db is None:
|
||||
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
|
||||
ipip_db = ipdb.City(ipip_db_path)
|
||||
|
||||
info = ipip_db.find_info(ip, 'CN')
|
||||
try:
|
||||
info = ipip_db.find_info(ip, 'CN')
|
||||
except ValueError:
|
||||
return None
|
||||
if not info:
|
||||
raise None
|
||||
return {'city': info.city_name, 'country': info.country_name}
|
||||
|
||||
@@ -74,13 +74,18 @@ def contains_ip(ip, ip_group):
|
||||
|
||||
|
||||
def get_ip_city(ip):
|
||||
info = get_ip_city_by_ipip(ip)
|
||||
city = info.get('city', _("Unknown"))
|
||||
country = info.get('country')
|
||||
if not ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
|
||||
# 国内城市 并且 语言是中文就使用国内
|
||||
is_zh = settings.LANGUAGE_CODE.startswith('zh')
|
||||
if country == '中国' and is_zh:
|
||||
return city
|
||||
else:
|
||||
return get_ip_city_by_geoip(ip)
|
||||
info = get_ip_city_by_ipip(ip)
|
||||
if info:
|
||||
city = info.get('city', _("Unknown"))
|
||||
country = info.get('country')
|
||||
|
||||
# 国内城市 并且 语言是中文就使用国内
|
||||
is_zh = settings.LANGUAGE_CODE.startswith('zh')
|
||||
if country == '中国' and is_zh:
|
||||
return city
|
||||
return get_ip_city_by_geoip(ip)
|
||||
|
||||
@@ -161,6 +161,7 @@ class Config(dict):
|
||||
'SESSION_COOKIE_AGE': 3600 * 24,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
|
||||
'LOGIN_URL': reverse_lazy('authentication:login'),
|
||||
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
|
||||
|
||||
# Custom Config
|
||||
# Auth LDAP settings
|
||||
@@ -191,6 +192,9 @@ class Config(dict):
|
||||
'AUTH_OPENID_CLIENT_AUTH_METHOD': 'client_secret_basic',
|
||||
'AUTH_OPENID_SHARE_SESSION': True,
|
||||
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
||||
'AUTH_OPENID_USER_ATTR_MAP': {
|
||||
'name': 'name', 'username': 'preferred_username', 'email': 'email'
|
||||
},
|
||||
|
||||
# OpenID 新配置参数 (version >= 1.5.9)
|
||||
'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://oidc.example.com/',
|
||||
@@ -320,8 +324,7 @@ class Config(dict):
|
||||
# 保留(Luna还在用)
|
||||
'TERMINAL_MAGNUS_ENABLED': True,
|
||||
'TERMINAL_KOKO_SSH_ENABLED': True,
|
||||
# 保留(Luna还在用)
|
||||
'XRDP_ENABLED': True,
|
||||
'TERMINAL_RAZOR_ENABLED': True,
|
||||
|
||||
# 安全配置
|
||||
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
|
||||
|
||||
@@ -73,6 +73,7 @@ AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE
|
||||
AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION
|
||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION
|
||||
AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER
|
||||
AUTH_OPENID_USER_ATTR_MAP = CONFIG.AUTH_OPENID_USER_ATTR_MAP
|
||||
AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login'
|
||||
AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback'
|
||||
AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:openid:logout'
|
||||
@@ -148,6 +149,11 @@ AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||
# Connection token
|
||||
CONNECTION_TOKEN_EXPIRATION = CONFIG.CONNECTION_TOKEN_EXPIRATION
|
||||
if CONNECTION_TOKEN_EXPIRATION < 5 * 60:
|
||||
# 最少5分钟
|
||||
CONNECTION_TOKEN_EXPIRATION = 5 * 60
|
||||
|
||||
|
||||
RBAC_BACKEND = 'rbac.backends.RBACBackend'
|
||||
|
||||
@@ -139,7 +139,7 @@ LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
|
||||
|
||||
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
|
||||
|
||||
XRDP_ENABLED = CONFIG.XRDP_ENABLED
|
||||
TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED
|
||||
TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED
|
||||
TERMINAL_KOKO_SSH_ENABLED = CONFIG.TERMINAL_KOKO_SSH_ENABLED
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5effe3cb5eb97d51bf886d21dfbe785bb789722f30774a6595eac7aa79b6315a
|
||||
size 127478
|
||||
oid sha256:132e7f59a56d1cf5b2358b21b547861e872fa456164f2e0809120fb2b13f0ec1
|
||||
size 128122
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d30f8d3abc215c35bb2dd889374eeef896a193f8010ccd5ae8e27aa408f045c7
|
||||
size 105337
|
||||
oid sha256:002f6953ebbe368642f0ea3c383f617b5f998edf2238341be63393123d4be8a9
|
||||
size 105894
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
|
||||
@@ -128,6 +128,7 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin,
|
||||
|
||||
nodes = PermNode.objects.none()
|
||||
assets = Asset.objects.none()
|
||||
all_tree_nodes = []
|
||||
|
||||
if not key:
|
||||
nodes = nodes_query_utils.get_top_level_nodes()
|
||||
@@ -142,7 +143,9 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin,
|
||||
|
||||
tree_nodes = self.serialize_nodes(nodes, with_asset_amount=True)
|
||||
tree_assets = self.serialize_assets(assets, key)
|
||||
return Response(data=[*tree_nodes, *tree_assets])
|
||||
all_tree_nodes.extend(tree_nodes)
|
||||
all_tree_nodes.extend(tree_assets)
|
||||
return Response(data=all_tree_nodes)
|
||||
|
||||
|
||||
class UserGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin):
|
||||
|
||||
@@ -9,14 +9,16 @@ from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class PermedAssetsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, assets):
|
||||
def __init__(self, user, assets, day_count=0):
|
||||
super().__init__(user)
|
||||
self.assets = assets
|
||||
self.day_count = day_count
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("You permed assets is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'items': [str(asset) for asset in self.assets],
|
||||
'item_type': _("permed assets"),
|
||||
'show_help': True
|
||||
@@ -38,10 +40,11 @@ class PermedAssetsWillExpireUserMsg(UserMessage):
|
||||
|
||||
class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
|
||||
def __init__(self, user, perms, org):
|
||||
def __init__(self, user, perms, org, day_count=0):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = day_count
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
@@ -59,6 +62,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
subject = _("Asset permissions is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'items_with_url': items_with_url,
|
||||
'item_type': _('asset permissions of organization {}').format(self.org)
|
||||
}
|
||||
@@ -81,14 +85,16 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
|
||||
|
||||
class PermedAppsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, apps):
|
||||
def __init__(self, user, apps, day_count=0):
|
||||
super().__init__(user)
|
||||
self.apps = apps
|
||||
self.day_count = day_count
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("Your permed applications is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'item_type': _('permed applications'),
|
||||
'items': [str(app) for app in self.apps]
|
||||
}
|
||||
@@ -109,10 +115,11 @@ class PermedAppsWillExpireUserMsg(UserMessage):
|
||||
|
||||
|
||||
class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
def __init__(self, user, perms, org):
|
||||
def __init__(self, user, perms, org, day_count=0):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = day_count
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
@@ -127,6 +134,7 @@ class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
subject = _('Application permissions is about to expire')
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'item_type': _('application permissions of organization {}').format(self.org),
|
||||
'items_with_url': items
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ def check_asset_permission_will_expired():
|
||||
start = local_now()
|
||||
end = start + timedelta(days=3)
|
||||
|
||||
user_asset_mapper = defaultdict(set)
|
||||
org_perm_mapper = defaultdict(set)
|
||||
user_asset_remain_day_mapper = defaultdict(dict)
|
||||
org_perm_remain_day_mapper = defaultdict(dict)
|
||||
|
||||
asset_perms = AssetPermission.objects.filter(
|
||||
date_expired__gte=start,
|
||||
@@ -72,23 +72,35 @@ def check_asset_permission_will_expired():
|
||||
).distinct()
|
||||
|
||||
for asset_perm in asset_perms:
|
||||
date_expired = dt_parser(asset_perm.date_expired)
|
||||
remain_days = (end - date_expired).days
|
||||
|
||||
org = asset_perm.org
|
||||
# 资产授权按照组织分类
|
||||
org_perm_mapper[asset_perm.org].add(asset_perm)
|
||||
if org in org_perm_remain_day_mapper[remain_days]:
|
||||
org_perm_remain_day_mapper[remain_days][org].add(asset_perm)
|
||||
else:
|
||||
org_perm_remain_day_mapper[remain_days][org] = {asset_perm, }
|
||||
|
||||
# 计算每个用户即将过期的资产
|
||||
users = asset_perm.get_all_users()
|
||||
assets = asset_perm.get_all_assets()
|
||||
|
||||
for u in users:
|
||||
user_asset_mapper[u].update(assets)
|
||||
if u in user_asset_remain_day_mapper[remain_days]:
|
||||
user_asset_remain_day_mapper[remain_days][u].update(assets)
|
||||
else:
|
||||
user_asset_remain_day_mapper[remain_days][u] = set(assets)
|
||||
|
||||
for user, assets in user_asset_mapper.items():
|
||||
PermedAssetsWillExpireUserMsg(user, assets).publish_async()
|
||||
for day_count, user_asset_mapper in user_asset_remain_day_mapper.items():
|
||||
for user, assets in user_asset_mapper.items():
|
||||
PermedAssetsWillExpireUserMsg(user, assets, day_count).publish_async()
|
||||
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
for org_admin in org_admins:
|
||||
AssetPermsWillExpireForOrgAdminMsg(org_admin, perms, org).publish_async()
|
||||
for day_count, org_perm_mapper in org_perm_remain_day_mapper.items():
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
for org_admin in org_admins:
|
||||
AssetPermsWillExpireForOrgAdminMsg(org_admin, perms, org, day_count).publish_async()
|
||||
|
||||
|
||||
@register_as_period_task(crontab='0 10 * * *')
|
||||
@@ -104,21 +116,33 @@ def check_app_permission_will_expired():
|
||||
date_expired__lte=end
|
||||
).distinct()
|
||||
|
||||
user_app_mapper = defaultdict(set)
|
||||
org_perm_mapper = defaultdict(set)
|
||||
user_app_remain_day_mapper = defaultdict(dict)
|
||||
org_perm_remain_day_mapper = defaultdict(dict)
|
||||
|
||||
for app_perm in app_perms:
|
||||
org_perm_mapper[app_perm.org].add(app_perm)
|
||||
date_expired = dt_parser(app_perm.date_expired)
|
||||
remain_days = (end - date_expired).days
|
||||
|
||||
org = app_perm.org
|
||||
if org in org_perm_remain_day_mapper[remain_days]:
|
||||
org_perm_remain_day_mapper[remain_days][org].add(app_perm)
|
||||
else:
|
||||
org_perm_remain_day_mapper[remain_days][org] = {app_perm, }
|
||||
|
||||
users = app_perm.get_all_users()
|
||||
apps = app_perm.applications.all()
|
||||
for u in users:
|
||||
user_app_mapper[u].update(apps)
|
||||
if u in user_app_remain_day_mapper[remain_days]:
|
||||
user_app_remain_day_mapper[remain_days][u].update(apps)
|
||||
else:
|
||||
user_app_remain_day_mapper[remain_days][u] = set(apps)
|
||||
|
||||
for user, apps in user_app_mapper.items():
|
||||
PermedAppsWillExpireUserMsg(user, apps).publish_async()
|
||||
for day_count, user_app_mapper in user_app_remain_day_mapper.items():
|
||||
for user, apps in user_app_mapper.items():
|
||||
PermedAppsWillExpireUserMsg(user, apps, day_count).publish_async()
|
||||
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
for org_admin in org_admins:
|
||||
AppPermsWillExpireForOrgAdminMsg(org_admin, perms, org).publish_async()
|
||||
for day_count, org_perm_mapper in org_perm_remain_day_mapper.items():
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
for org_admin in org_admins:
|
||||
AppPermsWillExpireForOrgAdminMsg(org_admin, perms, org, day_count).publish_async()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
The following {{ item_type }} will expire in 3 days
|
||||
The following {{ item_type }} will expire in {{ count }} days
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
The following {{ item_type }} will expire in 3 days
|
||||
The following {{ item_type }} will expire in {{ count }} days
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class GrantedAppTreeUtil:
|
||||
'title': name,
|
||||
'pId': '',
|
||||
'open': True,
|
||||
'iconSkin': 'applications',
|
||||
'isParent': True,
|
||||
'meta': {
|
||||
'type': 'root'
|
||||
@@ -35,6 +36,22 @@ class GrantedAppTreeUtil:
|
||||
})
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def create_empty_node():
|
||||
name = _("Empty")
|
||||
node = TreeNode(**{
|
||||
'id': 'empty',
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': '',
|
||||
'isParent': True,
|
||||
'children': [],
|
||||
'meta': {
|
||||
'type': 'application'
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def get_children_nodes(tree_id, parent_info, user):
|
||||
tree_nodes = []
|
||||
@@ -60,10 +77,9 @@ class GrantedAppTreeUtil:
|
||||
def create_tree_nodes(self, applications):
|
||||
tree_nodes = []
|
||||
if not applications:
|
||||
return tree_nodes
|
||||
return [self.create_empty_node()]
|
||||
|
||||
root_node = self.create_root_node()
|
||||
tree_nodes.append(root_node)
|
||||
organizations = self.filter_organizations(applications)
|
||||
|
||||
for i, org in enumerate(organizations):
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.db.models import output_as_string, UnionQuerySet
|
||||
from common.utils.common import lazyproperty, timeit
|
||||
@@ -614,6 +615,22 @@ class UserGrantedNodesQueryUtils(UserGrantedUtilsBase):
|
||||
assets_amount = assets_query_utils.get_favorite_assets().values_list('id').count()
|
||||
return PermNode.get_favorite_node(assets_amount)
|
||||
|
||||
@staticmethod
|
||||
def get_root_node():
|
||||
name = _('My assets')
|
||||
node = {
|
||||
'id': '',
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': '',
|
||||
'open': True,
|
||||
'isParent': True,
|
||||
'meta': {
|
||||
'type': 'root'
|
||||
}
|
||||
}
|
||||
return node
|
||||
|
||||
def get_special_nodes(self):
|
||||
nodes = []
|
||||
if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE:
|
||||
|
||||
@@ -39,6 +39,21 @@ class RoleViewSet(PaginatedResponseMixin, JMSModelViewSet):
|
||||
raise PermissionDenied(error)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
super(RoleViewSet, self).perform_create(serializer)
|
||||
self.set_permissions_if_need(serializer.instance)
|
||||
|
||||
def set_permissions_if_need(self, instance):
|
||||
if not isinstance(instance, Role):
|
||||
return
|
||||
clone_from = self.request.query_params.get('clone_from')
|
||||
if not clone_from:
|
||||
return
|
||||
clone = Role.objects.filter(id=clone_from).first()
|
||||
if not clone:
|
||||
return
|
||||
instance.permissions.set(clone.permissions.all())
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = serializer.instance
|
||||
if instance.builtin:
|
||||
|
||||
@@ -126,6 +126,8 @@ class BuiltinRole:
|
||||
org_user = PredefineRole(
|
||||
'7', ugettext_noop('OrgUser'), Scope.org, user_perms
|
||||
)
|
||||
system_role_mapper = None
|
||||
org_role_mapper = None
|
||||
|
||||
@classmethod
|
||||
def get_roles(cls):
|
||||
@@ -138,22 +140,24 @@ class BuiltinRole:
|
||||
|
||||
@classmethod
|
||||
def get_system_role_by_old_name(cls, name):
|
||||
mapper = {
|
||||
'App': cls.system_component,
|
||||
'Admin': cls.system_admin,
|
||||
'User': cls.system_user,
|
||||
'Auditor': cls.system_auditor
|
||||
}
|
||||
return mapper[name].get_role()
|
||||
if not cls.system_role_mapper:
|
||||
cls.system_role_mapper = {
|
||||
'App': cls.system_component.get_role(),
|
||||
'Admin': cls.system_admin.get_role(),
|
||||
'User': cls.system_user.get_role(),
|
||||
'Auditor': cls.system_auditor.get_role()
|
||||
}
|
||||
return cls.system_role_mapper[name]
|
||||
|
||||
@classmethod
|
||||
def get_org_role_by_old_name(cls, name):
|
||||
mapper = {
|
||||
'Admin': cls.org_admin,
|
||||
'User': cls.org_user,
|
||||
'Auditor': cls.org_auditor,
|
||||
}
|
||||
return mapper[name].get_role()
|
||||
if not cls.org_role_mapper:
|
||||
cls.org_role_mapper = {
|
||||
'Admin': cls.org_admin.get_role(),
|
||||
'User': cls.org_user.get_role(),
|
||||
'Auditor': cls.org_auditor.get_role(),
|
||||
}
|
||||
return cls.org_role_mapper[name]
|
||||
|
||||
@classmethod
|
||||
def sync_to_db(cls, show_msg=False):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Generated by Django 3.1.13 on 2021-12-01 11:01
|
||||
|
||||
import time
|
||||
from django.db import migrations
|
||||
|
||||
from rbac.builtin import BuiltinRole
|
||||
@@ -9,33 +10,61 @@ def migrate_system_role_binding(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
user_model = apps.get_model('users', 'User')
|
||||
role_binding_model = apps.get_model('rbac', 'SystemRoleBinding')
|
||||
users = user_model.objects.using(db_alias).all()
|
||||
|
||||
role_bindings = []
|
||||
for user in users:
|
||||
role = BuiltinRole.get_system_role_by_old_name(user.role)
|
||||
role_binding = role_binding_model(scope='system', user_id=user.id, role_id=role.id)
|
||||
role_bindings.append(role_binding)
|
||||
role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
|
||||
count = 0
|
||||
bulk_size = 1000
|
||||
while True:
|
||||
users = user_model.objects.using(db_alias) \
|
||||
.only('role', 'id') \
|
||||
.all()[count:count+bulk_size]
|
||||
if not users:
|
||||
break
|
||||
|
||||
role_bindings = []
|
||||
start = time.time()
|
||||
for user in users:
|
||||
role = BuiltinRole.get_system_role_by_old_name(user.role)
|
||||
role_binding = role_binding_model(scope='system', user_id=user.id, role_id=role.id)
|
||||
role_bindings.append(role_binding)
|
||||
|
||||
role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
|
||||
print("Create role binding: {}-{} using: {:.2f}s".format(
|
||||
count, count + len(users), time.time()-start
|
||||
))
|
||||
count += len(users)
|
||||
|
||||
|
||||
def migrate_org_role_binding(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
org_member_model = apps.get_model('orgs', 'OrganizationMember')
|
||||
role_binding_model = apps.get_model('rbac', 'RoleBinding')
|
||||
members = org_member_model.objects.using(db_alias).all()
|
||||
|
||||
role_bindings = []
|
||||
for member in members:
|
||||
role = BuiltinRole.get_org_role_by_old_name(member.role)
|
||||
role_binding = role_binding_model(
|
||||
scope='org',
|
||||
user_id=member.user.id,
|
||||
role_id=role.id,
|
||||
org_id=member.org.id
|
||||
)
|
||||
role_bindings.append(role_binding)
|
||||
role_binding_model.objects.bulk_create(role_bindings)
|
||||
count = 0
|
||||
bulk_size = 1000
|
||||
|
||||
while True:
|
||||
members = org_member_model.objects.using(db_alias)\
|
||||
.only('role', 'user_id', 'org_id')\
|
||||
.all()[count:count+bulk_size]
|
||||
if not members:
|
||||
break
|
||||
role_bindings = []
|
||||
start = time.time()
|
||||
|
||||
for member in members:
|
||||
role = BuiltinRole.get_org_role_by_old_name(member.role)
|
||||
role_binding = role_binding_model(
|
||||
scope='org',
|
||||
user_id=member.user_id,
|
||||
role_id=role.id,
|
||||
org_id=member.org_id
|
||||
)
|
||||
role_bindings.append(role_binding)
|
||||
role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
|
||||
print("Create role binding: {}-{} using: {:.2f}s".format(
|
||||
count, count + len(members), time.time()-start
|
||||
))
|
||||
count += len(members)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
23
apps/settings/migrations/0006_remove_setting_enabled.py
Normal file
23
apps/settings/migrations/0006_remove_setting_enabled.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.14 on 2022-06-06 09:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_terminal_razor_enabled(apps, schema_editor):
|
||||
setting_model = apps.get_model("settings", "Setting")
|
||||
s = setting_model.objects.filter(name='XRDP_ENABLED').first()
|
||||
if not s:
|
||||
return
|
||||
s.name = 'TERMINAL_RAZOR_ENABLED'
|
||||
s.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('settings', '0005_auto_20220310_0616'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_terminal_razor_enabled),
|
||||
]
|
||||
@@ -1,11 +1,13 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = ['DingTalkSettingSerializer']
|
||||
|
||||
|
||||
class DingTalkSettingSerializer(serializers.Serializer):
|
||||
DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId')
|
||||
DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey')
|
||||
DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label='AppSecret', write_only=True)
|
||||
DINGTALK_APPSECRET = EncryptedField(max_length=256, required=False, label='AppSecret')
|
||||
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth'))
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = ['FeiShuSettingSerializer']
|
||||
|
||||
|
||||
class FeiShuSettingSerializer(serializers.Serializer):
|
||||
FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID')
|
||||
FEISHU_APP_SECRET = serializers.CharField(max_length=256, required=False, label='App Secret', write_only=True)
|
||||
FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
|
||||
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth'))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ __all__ = [
|
||||
class LDAPTestConfigSerializer(serializers.Serializer):
|
||||
AUTH_LDAP_SERVER_URI = serializers.CharField(max_length=1024)
|
||||
AUTH_LDAP_BIND_DN = serializers.CharField(max_length=1024, required=False, allow_blank=True)
|
||||
AUTH_LDAP_BIND_PASSWORD = serializers.CharField(required=False, allow_blank=True)
|
||||
AUTH_LDAP_BIND_PASSWORD = EncryptedField(required=False, allow_blank=True)
|
||||
AUTH_LDAP_SEARCH_OU = serializers.CharField()
|
||||
AUTH_LDAP_SEARCH_FILTER = serializers.CharField()
|
||||
AUTH_LDAP_USER_ATTR_MAP = serializers.CharField()
|
||||
@@ -42,8 +42,8 @@ class LDAPSettingSerializer(serializers.Serializer):
|
||||
help_text=_('eg: ldap://localhost:389')
|
||||
)
|
||||
AUTH_LDAP_BIND_DN = serializers.CharField(required=False, max_length=1024, label=_('Bind DN'))
|
||||
AUTH_LDAP_BIND_PASSWORD = serializers.CharField(
|
||||
max_length=1024, write_only=True, required=False, label=_('Password')
|
||||
AUTH_LDAP_BIND_PASSWORD = EncryptedField(
|
||||
max_length=1024, required=False, label=_('Password')
|
||||
)
|
||||
AUTH_LDAP_SEARCH_OU = serializers.CharField(
|
||||
max_length=1024, allow_blank=True, required=False, label=_('User OU'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = [
|
||||
'OIDCSettingSerializer', 'KeycloakSettingSerializer',
|
||||
]
|
||||
@@ -14,8 +16,8 @@ class CommonSettingSerializer(serializers.Serializer):
|
||||
AUTH_OPENID_CLIENT_ID = serializers.CharField(
|
||||
required=False, max_length=1024, label=_('Client Id')
|
||||
)
|
||||
AUTH_OPENID_CLIENT_SECRET = serializers.CharField(
|
||||
required=False, max_length=1024, write_only=True, label=_('Client Secret')
|
||||
AUTH_OPENID_CLIENT_SECRET = EncryptedField(
|
||||
required=False, max_length=1024, label=_('Client Secret')
|
||||
)
|
||||
AUTH_OPENID_CLIENT_AUTH_METHOD = serializers.ChoiceField(
|
||||
default='client_secret_basic',
|
||||
@@ -29,6 +31,11 @@ class CommonSettingSerializer(serializers.Serializer):
|
||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = serializers.BooleanField(
|
||||
required=False, label=_('Ignore ssl verification')
|
||||
)
|
||||
AUTH_OPENID_USER_ATTR_MAP = serializers.DictField(
|
||||
required=True, label=_('User attr map'),
|
||||
help_text=_('User attr map present how to map OpenID user attr to '
|
||||
'jumpserver, username,name,email is jumpserver attr')
|
||||
)
|
||||
|
||||
|
||||
class KeycloakSettingSerializer(CommonSettingSerializer):
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
__all__ = [
|
||||
'RadiusSettingSerializer',
|
||||
]
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = ['RadiusSettingSerializer']
|
||||
|
||||
|
||||
class RadiusSettingSerializer(serializers.Serializer):
|
||||
AUTH_RADIUS = serializers.BooleanField(required=False, label=_('Enable Radius Auth'))
|
||||
RADIUS_SERVER = serializers.CharField(required=False, allow_blank=True, max_length=1024, label=_('Host'))
|
||||
RADIUS_PORT = serializers.IntegerField(required=False, label=_('Port'))
|
||||
RADIUS_SECRET = serializers.CharField(
|
||||
required=False, max_length=1024, allow_null=True, label=_('Secret'), write_only=True
|
||||
RADIUS_SECRET = EncryptedField(
|
||||
required=False, max_length=1024, allow_null=True, label=_('Secret'),
|
||||
)
|
||||
OTP_IN_RADIUS = serializers.BooleanField(required=False, label=_('OTP in Radius'))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from common.sdk.sms import BACKENDS
|
||||
|
||||
__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
|
||||
@@ -29,8 +30,8 @@ class BaseSMSSettingSerializer(serializers.Serializer):
|
||||
|
||||
class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='AccessKeyId')
|
||||
ALIBABA_ACCESS_KEY_SECRET = serializers.CharField(
|
||||
max_length=256, required=False, label='AccessKeySecret', write_only=True
|
||||
ALIBABA_ACCESS_KEY_SECRET = EncryptedField(
|
||||
max_length=256, required=False, label='AccessKeySecret',
|
||||
)
|
||||
ALIBABA_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
ALIBABA_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
@@ -38,7 +39,7 @@ class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
|
||||
class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
TENCENT_SECRET_ID = serializers.CharField(max_length=256, required=True, label='Secret id')
|
||||
TENCENT_SECRET_KEY = serializers.CharField(max_length=256, required=False, label='Secret key', write_only=True)
|
||||
TENCENT_SECRET_KEY = EncryptedField(max_length=256, required=False, label='Secret key')
|
||||
TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
|
||||
TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = ['WeComSettingSerializer']
|
||||
|
||||
|
||||
class WeComSettingSerializer(serializers.Serializer):
|
||||
WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid')
|
||||
WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid')
|
||||
WECOM_SECRET = serializers.CharField(max_length=256, required=False, label='secret', write_only=True)
|
||||
AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
|
||||
WECOM_SECRET = EncryptedField(max_length=256, required=False, label='secret')
|
||||
AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = ['MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer']
|
||||
|
||||
|
||||
@@ -18,8 +20,8 @@ class EmailSettingSerializer(serializers.Serializer):
|
||||
EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host"))
|
||||
EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port"))
|
||||
EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("SMTP account"))
|
||||
EMAIL_HOST_PASSWORD = serializers.CharField(
|
||||
max_length=1024, write_only=True, required=False, label=_("SMTP password"),
|
||||
EMAIL_HOST_PASSWORD = EncryptedField(
|
||||
max_length=1024, required=False, label=_("SMTP password"),
|
||||
help_text=_("Tips: Some provider use token except password")
|
||||
)
|
||||
EMAIL_FROM = serializers.CharField(
|
||||
|
||||
@@ -35,11 +35,11 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
AUTH_FEISHU = serializers.BooleanField()
|
||||
AUTH_TEMP_TOKEN = serializers.BooleanField()
|
||||
|
||||
XRDP_ENABLED = serializers.BooleanField()
|
||||
TERMINAL_RAZOR_ENABLED = serializers.BooleanField()
|
||||
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField()
|
||||
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField()
|
||||
|
||||
ANNOUNCEMENT_ENABLED = serializers.BooleanField()
|
||||
ANNOUNCEMENT = serializers.CharField()
|
||||
ANNOUNCEMENT = serializers.DictField()
|
||||
|
||||
TICKETS_ENABLED = serializers.BooleanField()
|
||||
|
||||
@@ -34,5 +34,5 @@ class TerminalSettingSerializer(serializers.Serializer):
|
||||
"if you cannot log in to the device through Telnet, set this parameter")
|
||||
)
|
||||
TERMINAL_MAGNUS_ENABLED = serializers.BooleanField(label=_("Enable database proxy"))
|
||||
XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP"))
|
||||
TERMINAL_RAZOR_ENABLED = serializers.BooleanField(label=_("Enable Razor"))
|
||||
TERMINAL_KOKO_SSH_ENABLED = serializers.BooleanField(label=_("Enable SSH Client"))
|
||||
|
||||
@@ -15,14 +15,15 @@ p {
|
||||
</style>
|
||||
<div style="margin: 0 200px">
|
||||
<div class="group">
|
||||
<h2>JumpServer {% trans 'Client' %} v1.1.5</h2>
|
||||
<h2>JumpServer {% trans 'Client' %} v1.1.6</h2>
|
||||
<p>
|
||||
{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %}
|
||||
</p>
|
||||
<ul>
|
||||
<li> <a href="/download/JumpServer-Client-Installer-x86_64.msi">jumpserver-client-windows-x86_64.msi</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer-arm64.msi">jumpserver-client-windows-arm64.msi</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer.dmg">jumpserver-client-darwin.dmg</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer-amd64.run">jumpserver-client-linux-amd64.run</a></li>
|
||||
<li> <a href="/download/JumpServer-Client-Installer-arm64.run">jumpserver-client-linux-arm64.run</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ class SessionJoinRecordsViewSet(OrgModelViewSet):
|
||||
)
|
||||
filterset_fields = search_fields
|
||||
model = models.SessionJoinRecord
|
||||
rbac_perms = {
|
||||
'finished': 'terminal.change_sessionjoinrecord'
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -297,6 +297,9 @@ class QuerySet(DJQuerySet):
|
||||
self._command_store_config = command_store_config
|
||||
self._storage = CommandStore(command_store_config)
|
||||
|
||||
# 命令列表模糊搜索时报错
|
||||
super().__init__()
|
||||
|
||||
@lazyproperty
|
||||
def _grouped_method_calls(self):
|
||||
_method_calls = {k: list(v) for k, v in groupby(self._method_calls, lambda x: x[0])}
|
||||
|
||||
@@ -49,6 +49,7 @@ class TerminalTypeChoices(TextChoices):
|
||||
core = 'core', 'Core'
|
||||
celery = 'celery', 'Celery'
|
||||
magnus = 'magnus', 'Magnus'
|
||||
razor = 'razor', 'Razor'
|
||||
|
||||
@classmethod
|
||||
def types(cls):
|
||||
|
||||
@@ -21,7 +21,7 @@ def migrate_endpoints(apps, schema_editor):
|
||||
}
|
||||
Endpoint.objects.create(**default_data)
|
||||
|
||||
if not settings.XRDP_ENABLED:
|
||||
if not settings.TERMINAL_RAZOR_ENABLED:
|
||||
return
|
||||
# migrate xrdp
|
||||
xrdp_addr = settings.TERMINAL_RDP_ADDR
|
||||
@@ -41,7 +41,7 @@ def migrate_endpoints(apps, schema_editor):
|
||||
else:
|
||||
rdp_port = 3389
|
||||
xrdp_data = {
|
||||
'name': 'XRDP',
|
||||
'name': 'Razor',
|
||||
'host': host,
|
||||
'https_port': 0,
|
||||
'http_port': 0,
|
||||
@@ -56,7 +56,7 @@ def migrate_endpoints(apps, schema_editor):
|
||||
|
||||
EndpointRule = apps.get_model("terminal", "EndpointRule")
|
||||
xrdp_rule_data = {
|
||||
'name': 'XRDP',
|
||||
'name': 'Razor',
|
||||
'ip_group': ['*'],
|
||||
'priority': 20,
|
||||
'endpoint': xrdp_endpoint,
|
||||
|
||||
18
apps/terminal/migrations/0050_auto_20220606_1745.py
Normal file
18
apps/terminal/migrations/0050_auto_20220606_1745.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.14 on 2022-06-06 09:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('terminal', '0049_endpoint_redis_port'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='terminal',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), ('razor', 'Razor')], default='koko', max_length=64, verbose_name='type'),
|
||||
),
|
||||
]
|
||||
@@ -1,14 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from urllib.parse import urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models import TextChoices
|
||||
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from common.drf.fields import ReadableHiddenField
|
||||
from common.drf.fields import ReadableHiddenField, EncryptedField
|
||||
from ..models import ReplayStorage, CommandStorage
|
||||
from .. import const
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
|
||||
# Replay storage serializers
|
||||
@@ -25,12 +26,12 @@ class ReplayStorageTypeBaseSerializer(serializers.Serializer):
|
||||
required=True, max_length=1024, label=_('Bucket'), allow_null=True
|
||||
)
|
||||
ACCESS_KEY = serializers.CharField(
|
||||
max_length=1024, required=False, allow_blank=True, write_only=True, label=_('Access key'),
|
||||
allow_null=True,
|
||||
max_length=1024, required=False, allow_blank=True,
|
||||
label=_('Access key id'), allow_null=True,
|
||||
)
|
||||
SECRET_KEY = serializers.CharField(
|
||||
max_length=1024, required=False, allow_blank=True, write_only=True, label=_('Secret key'),
|
||||
allow_null=True,
|
||||
SECRET_KEY = EncryptedField(
|
||||
max_length=1024, required=False, allow_blank=True,
|
||||
label=_('Access key secret'), allow_null=True,
|
||||
)
|
||||
ENDPOINT = serializers.CharField(
|
||||
validators=[replay_storage_endpoint_format_validator],
|
||||
@@ -108,7 +109,7 @@ class ReplayStorageTypeAzureSerializer(serializers.Serializer):
|
||||
max_length=1024, label=_('Container name'), allow_null=True
|
||||
)
|
||||
ACCOUNT_NAME = serializers.CharField(max_length=1024, label=_('Account name'), allow_null=True)
|
||||
ACCOUNT_KEY = serializers.CharField(max_length=1024, label=_('Account key'), allow_null=True)
|
||||
ACCOUNT_KEY = EncryptedField(max_length=1024, label=_('Account key'), allow_null=True)
|
||||
ENDPOINT_SUFFIX = serializers.ChoiceField(
|
||||
choices=EndpointSuffixChoices.choices, default=EndpointSuffixChoices.china.value,
|
||||
label=_('Endpoint suffix'), allow_null=True,
|
||||
|
||||
@@ -791,6 +791,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
||||
def is_local(self):
|
||||
return self.source == self.Source.local.value
|
||||
|
||||
def is_password_authenticate(self):
|
||||
cas = self.Source.cas
|
||||
saml2 = self.Source.saml2
|
||||
return self.source not in [cas, saml2]
|
||||
|
||||
def set_unprovide_attr_if_need(self):
|
||||
if not self.name:
|
||||
self.name = self.username
|
||||
|
||||
@@ -143,7 +143,7 @@ class PasswordExpirationReminderMsg(UserMessage):
|
||||
subject = _('Password is about expire')
|
||||
|
||||
date_password_expired_local = timezone.localtime(user.date_password_expired)
|
||||
update_password_url = urljoin(settings.SITE_URL, '/ui/#/users/profile/?activeTab=PasswordUpdate')
|
||||
update_password_url = urljoin(settings.SITE_URL, '/ui/#/profile/setting/?activeTab=PasswordUpdate')
|
||||
date_password_expired = date_password_expired_local.strftime('%Y-%m-%d %H:%M:%S')
|
||||
context = {
|
||||
'name': user.name,
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from .utils import is_auth_password_time_valid
|
||||
from .utils import is_auth_password_time_valid, is_auth_confirm_time_valid
|
||||
|
||||
|
||||
class IsAuthPasswdTimeValid(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and is_auth_password_time_valid(request.session)
|
||||
and is_auth_password_time_valid(request.session)
|
||||
|
||||
|
||||
class IsAuthConfirmTimeValid(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and is_auth_confirm_time_valid(request.session)
|
||||
|
||||
@@ -17,9 +17,9 @@ class UserOrgSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class UserUpdatePasswordSerializer(serializers.ModelSerializer):
|
||||
old_password = EncryptedField(required=True, max_length=128, write_only=True)
|
||||
new_password = EncryptedField(required=True, max_length=128, write_only=True)
|
||||
new_password_again = EncryptedField(required=True, max_length=128, write_only=True)
|
||||
old_password = EncryptedField(required=True, max_length=128)
|
||||
new_password = EncryptedField(required=True, max_length=128)
|
||||
new_password_again = EncryptedField(required=True, max_length=128)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@@ -57,18 +57,20 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class UserUpdateSecretKeySerializer(serializers.ModelSerializer):
|
||||
new_secret_key = serializers.CharField(required=True, max_length=128, write_only=True)
|
||||
new_secret_key_again = serializers.CharField(required=True, max_length=128, write_only=True)
|
||||
new_secret_key = EncryptedField(required=True, max_length=128)
|
||||
new_secret_key_again = EncryptedField(required=True, max_length=128)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['new_secret_key', 'new_secret_key_again']
|
||||
|
||||
def validate_new_secret_key_again(self, value):
|
||||
if value != self.initial_data.get('new_secret_key', ''):
|
||||
def validate(self, values):
|
||||
new_secret_key = values.get('new_secret_key', '')
|
||||
new_secret_key_again = values.get('new_secret_key_again', '')
|
||||
if new_secret_key != new_secret_key_again:
|
||||
msg = _('The newly set password is inconsistent')
|
||||
raise serializers.ValidationError(msg)
|
||||
return value
|
||||
raise serializers.ValidationError({'new_secret_key_again': msg})
|
||||
return values
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
new_secret_key = self.validated_data.get('new_secret_key')
|
||||
|
||||
@@ -255,3 +255,16 @@ def is_auth_password_time_valid(session):
|
||||
|
||||
def is_auth_otp_time_valid(session):
|
||||
return is_auth_time_valid(session, 'auth_otp_expired_at')
|
||||
|
||||
|
||||
def is_confirm_time_valid(session, key):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return True
|
||||
mfa_verify_time = session.get(key, 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_auth_confirm_time_valid(session):
|
||||
return is_confirm_time_valid(session, 'MFA_VERIFY_TIME')
|
||||
|
||||
17
jms
17
jms
@@ -8,6 +8,7 @@ import time
|
||||
import argparse
|
||||
import sys
|
||||
import django
|
||||
import requests
|
||||
from django.core import management
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
@@ -33,6 +34,7 @@ except ImportError as e:
|
||||
|
||||
try:
|
||||
from jumpserver.const import CONFIG
|
||||
from common.utils.file import download_file
|
||||
except ImportError as e:
|
||||
print("Import error: {}".format(e))
|
||||
print("Could not find config file, `cp config_example.yml config.yml`")
|
||||
@@ -105,6 +107,20 @@ def compile_i18n_file():
|
||||
logging.info("Compile i18n files done")
|
||||
|
||||
|
||||
def download_ip_db():
|
||||
db_base_dir = os.path.join(APP_DIR, 'common', 'utils', 'ip')
|
||||
db_path_url_mapper = {
|
||||
('geoip', 'GeoLite2-City.mmdb'): 'https://jms-pkg.oss-cn-beijing.aliyuncs.com/ip/GeoLite2-City.mmdb',
|
||||
('ipip', 'ipipfree.ipdb'): 'https://jms-pkg.oss-cn-beijing.aliyuncs.com/ip/ipipfree.ipdb'
|
||||
}
|
||||
for p, src in db_path_url_mapper.items():
|
||||
path = os.path.join(db_base_dir, *p)
|
||||
if os.path.isfile(path) and os.path.getsize(path) > 1000:
|
||||
continue
|
||||
print("Download ip db: {}".format(path))
|
||||
download_file(src, path)
|
||||
|
||||
|
||||
def upgrade_db():
|
||||
collect_static()
|
||||
perform_db_migrate()
|
||||
@@ -114,6 +130,7 @@ def prepare():
|
||||
check_database_connection()
|
||||
upgrade_db()
|
||||
expire_caches()
|
||||
download_ip_db()
|
||||
|
||||
|
||||
def start_services():
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
amqp==5.0.9
|
||||
ansible==2.10
|
||||
ansible==2.10.7
|
||||
asn1crypto==0.24.0
|
||||
bcrypt==3.1.4
|
||||
billiard==3.6.4.0
|
||||
@@ -59,7 +59,7 @@ PyNaCl==1.5.0
|
||||
python-dateutil==2.8.2
|
||||
pytz==2018.3
|
||||
PyYAML==6.0
|
||||
redis==3.5.3
|
||||
redis==4.3.1
|
||||
requests==2.25.1
|
||||
jms-storage==0.0.42
|
||||
s3transfer==0.5.0
|
||||
|
||||
@@ -15,8 +15,7 @@ django.setup()
|
||||
from resources.assets import AssetsGenerator, NodesGenerator, SystemUsersGenerator, AdminUsersGenerator
|
||||
from resources.users import UserGroupGenerator, UserGenerator
|
||||
from resources.perms import AssetPermissionGenerator
|
||||
from resources.terminal import CommandGenerator
|
||||
# from resources.system import StatGenerator
|
||||
from resources.terminal import CommandGenerator, SessionGenerator
|
||||
|
||||
|
||||
resource_generator_mapper = {
|
||||
@@ -28,6 +27,7 @@ resource_generator_mapper = {
|
||||
'user_group': UserGroupGenerator,
|
||||
'asset_permission': AssetPermissionGenerator,
|
||||
'command': CommandGenerator,
|
||||
'session': SessionGenerator
|
||||
# 'stat': StatGenerator
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import random
|
||||
|
||||
from .base import FakeDataGenerator
|
||||
from system.models import *
|
||||
from terminal.models import *
|
||||
|
||||
|
||||
class StatGenerator(FakeDataGenerator):
|
||||
@@ -66,4 +66,4 @@ class StatGenerator(FakeDataGenerator):
|
||||
'datetime': datetime
|
||||
})
|
||||
items.append(Stat(**node))
|
||||
Stat.objects.bulk_create(items, ignore_conflicts=True)
|
||||
Stat.objects.bulk_create(items, ignore_conflicts=True)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from .base import FakeDataGenerator
|
||||
from terminal.models import Command
|
||||
from users.models import *
|
||||
from assets.models import *
|
||||
from terminal.models import *
|
||||
import forgery_py
|
||||
import random
|
||||
|
||||
|
||||
class CommandGenerator(FakeDataGenerator):
|
||||
@@ -8,3 +12,43 @@ class CommandGenerator(FakeDataGenerator):
|
||||
def do_generate(self, batch, batch_size):
|
||||
Command.generate_fake(len(batch), self.org)
|
||||
|
||||
|
||||
class SessionGenerator(FakeDataGenerator):
|
||||
resource = 'session'
|
||||
users: list
|
||||
assets: list
|
||||
system_users: list
|
||||
|
||||
def pre_generate(self):
|
||||
self.users = list(User.objects.all())
|
||||
self.assets = list(Asset.objects.all())
|
||||
self.system_users = list(SystemUser.objects.all())
|
||||
|
||||
def do_generate(self, batch, batch_size):
|
||||
user = random.choice(self.users)
|
||||
asset = random.choice(self.assets)
|
||||
system_user = random.choice(self.system_users)
|
||||
|
||||
objects = []
|
||||
|
||||
now = timezone.now()
|
||||
timedelta = list(range(30))
|
||||
for i in batch:
|
||||
delta = random.choice(timedelta)
|
||||
date_start = now - timezone.timedelta(days=delta, seconds=delta * 60)
|
||||
date_end = date_start + timezone.timedelta(seconds=delta * 60)
|
||||
data = dict(
|
||||
user=user.name,
|
||||
user_id=user.id,
|
||||
asset=asset.hostname,
|
||||
asset_id=asset.id,
|
||||
system_user=system_user.name,
|
||||
system_user_id=system_user.id,
|
||||
org_id=self.org.id,
|
||||
date_start=date_start,
|
||||
date_end=date_end,
|
||||
is_finished=True
|
||||
)
|
||||
objects.append(Session(**data))
|
||||
creates = Session.objects.bulk_create(objects, ignore_conflicts=True)
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ class UserGenerator(FakeDataGenerator):
|
||||
group_ids: list
|
||||
|
||||
def pre_generate(self):
|
||||
self.roles = list(dict(User.ROLE.choices).keys())
|
||||
self.group_ids = list(UserGroup.objects.all().values_list('id', flat=True))
|
||||
|
||||
def set_org(self, users):
|
||||
@@ -53,7 +52,6 @@ class UserGenerator(FakeDataGenerator):
|
||||
username=username,
|
||||
email=email,
|
||||
name=username.title(),
|
||||
role=choice(self.roles),
|
||||
created_by='Faker'
|
||||
)
|
||||
users.append(u)
|
||||
|
||||
68
utils/test_run_migrations.py
Normal file
68
utils/test_run_migrations.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Generated by Django 3.1.13 on 2021-12-01 11:01
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
import time
|
||||
|
||||
app_path = '***** Change me *******'
|
||||
sys.path.insert(0, app_path)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
|
||||
django.setup()
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import connection
|
||||
|
||||
# ========================== 添加到需要测试的 migrations 上方 ==========================
|
||||
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from rbac.builtin import BuiltinRole
|
||||
|
||||
|
||||
def migrate_system_role_binding(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
user_model = apps.get_model('users', 'User')
|
||||
role_binding_model = apps.get_model('rbac', 'SystemRoleBinding')
|
||||
|
||||
count = 0
|
||||
bulk_size = 1000
|
||||
while True:
|
||||
users = user_model.objects.using(db_alias) \
|
||||
.only('role', 'id') \
|
||||
.all()[count:count+bulk_size]
|
||||
if not users:
|
||||
break
|
||||
|
||||
role_bindings = []
|
||||
start = time.time()
|
||||
for user in users:
|
||||
role = BuiltinRole.get_system_role_by_old_name(user.role)
|
||||
role_binding = role_binding_model(scope='system', user_id=user.id, role_id=role.id)
|
||||
role_bindings.append(role_binding)
|
||||
|
||||
role_binding_model.objects.bulk_create(role_bindings, ignore_conflicts=True)
|
||||
print("Create role binding: {}-{} using: {:.2f}s".format(
|
||||
count, count + len(users), time.time()-start
|
||||
))
|
||||
count += len(users)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rbac', '0003_auto_20211130_1037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_system_role_binding),
|
||||
]
|
||||
|
||||
|
||||
# ================== 添加到下方 ======================
|
||||
def main():
|
||||
schema_editor = connection.schema_editor()
|
||||
migrate_system_role_binding(apps, schema_editor)
|
||||
|
||||
|
||||
# main()
|
||||
Reference in New Issue
Block a user