Compare commits

...

118 Commits
v3.8 ... v3.9

Author SHA1 Message Date
fit2bot
43dc58c06c perf: dockerfile 添加 freerdp2-dev 依赖 (#12387)
Co-authored-by: feng <1304903146@qq.com>
2023-12-20 18:49:01 +08:00
ibuler
2a23af5030 perf: 优化 applet 账号选择 2023-12-05 16:19:19 +08:00
wangruidong
0fbbdceec2 fix: 双机主备部署sftp备份文件找不到 2023-12-05 14:01:26 +08:00
wangruidong
b10ee436e8 fix: sftp不能设置为默认存储 2023-11-28 15:19:00 +08:00
ibuler
24987c7f60 perf: 优化 queryset count 2023-11-28 12:53:32 +08:00
feng
73bed4d33d fix: mysql 开始ssl后 再关闭测试失败 2023-11-20 15:39:06 +08:00
feng
4c389c05c1 fix: mysql 开启ssl 再关闭 测试可连接性失败 2023-11-20 10:50:56 +08:00
ibuler
e744c4c8af perf: 优化延迟运行
fix: 延迟执行设置超时

perf: 修改 delay run

perf: 优化 delay_run 执行

perf: 修改 delay run
2023-11-20 10:31:20 +08:00
feng
06d1c9f420 fix: redis 开启 ssl websocket连接失败 2023-11-20 10:23:03 +08:00
ibuler
a92023840a fix: 修复自动禁用非活跃用户任务 2023-11-17 15:41:12 +08:00
Aaron3S
2d1bf866fa fix: 删除debug信息 2023-11-17 11:34:58 +08:00
吴小白
c606c3eb21 perf: 优化 Dockerfile 2023-11-17 10:38:41 +08:00
老广
dabbb45f6e Merge pull request #12144 from jumpserver/dev
v3.9.0
2023-11-16 18:23:05 +08:00
ibuler
ded1b4bba1 perf: 优化 api key 认证记录用户的时间 2023-11-16 18:17:22 +08:00
fit2bot
2630ea39a1 perf: windows 改密推送添加新的方式 最后测试可连接性的时候采用rdp的方式测试 (#12141)
Co-authored-by: feng <1304903146@qq.com>
2023-11-16 18:12:22 +08:00
Bryan
9e10029bdd Revert "fix: 修复平台自动化翻译 (#12078)" (#12138)
This reverts commit 69c0eb2f50.
2023-11-16 16:35:08 +08:00
Aaron3S
d1391cb5d5 fix: 修复 sqlserver 命令执行问题 2023-11-16 16:24:39 +08:00
Aaron3S
44f029774d fix: 修复playbook部分不可执行问题 2023-11-16 16:07:31 +08:00
fit2bot
23fce9e426 perf: 翻译 (#12135)
Co-authored-by: feng <1304903146@qq.com>
2023-11-16 15:35:34 +08:00
fit2bot
0778a39894 perf: 在线会话添加活跃状态过滤 (#12134)
Co-authored-by: feng <1304903146@qq.com>
2023-11-16 14:41:35 +08:00
fit2bot
9cc6d6a9af perf: dockerfile add libx11-dev (#12133)
Co-authored-by: feng <1304903146@qq.com>
2023-11-16 13:21:16 +08:00
fit2bot
8f309dee92 fix: 资产测试可连接性选错账号 (#12130)
Co-authored-by: feng <1304903146@qq.com>
2023-11-16 11:26:05 +08:00
Bai
d166b26252 perf: 优化处理telnet协议资产端点的端口问题 2023-11-16 11:13:37 +08:00
fit2bot
1ef51563b5 perf: account 迁移文件 (#12128)
Co-authored-by: feng <1304903146@qq.com>
2023-11-16 10:18:54 +08:00
老广
3e7b4682e4 Merge pull request #12124 from jumpserver/pr@dev@perf_device_icon
perf: 修改 tree 硬件设备的 icon
2023-11-15 17:02:05 +08:00
ibuler
994b42aa93 perf: 修改 tree 硬件设备的 icon 2023-11-15 17:00:12 +08:00
fit2bot
d6aea54722 fix: 账号收集未同步资产时 变更数据错误 (#12123)
Co-authored-by: feng <1304903146@qq.com>
2023-11-15 16:44:35 +08:00
ibuler
88afabdd1d perf: 设置 winrm 用户端不可以连接 2023-11-15 15:34:38 +08:00
fit2bot
b2327c0c5a fix: 账号改密 root密钥无法替换 (#12121)
Co-authored-by: feng <1304903146@qq.com>
2023-11-15 15:33:10 +08:00
Aaron3S
7610f64433 perf: 优化获取当前 python 执行路径的方式 2023-11-15 15:21:56 +08:00
fit2bot
b15c314384 fix: 资产多协议时 计算协议端口错误 (#12120)
Co-authored-by: feng <1304903146@qq.com>
2023-11-15 14:59:40 +08:00
wangruidong
7a5cffac91 fix: 对象存储下拉无法自动加载 2023-11-15 14:58:33 +08:00
feng
8667943443 fix: celery事物 数据库未保存 2023-11-14 19:42:21 +08:00
Aaron3S
7c51d90a3d fix: 修复快捷命令找不到mssql module 的问题 2023-11-14 19:28:46 +08:00
wangruidong
9996b200f9 fix: 作业执行历史日志未按配置天数清理 2023-11-14 19:22:14 +08:00
wangruidong
ae364ac373 fix: 录像存储下载报错 2023-11-14 19:21:33 +08:00
wangruidong
fef4a97931 fix: 作业日志筛选用户出错 2023-11-14 19:20:52 +08:00
fit2bot
d63c4d6cc4 fix: mysql 测试可连接性失败 (#12104)
Co-authored-by: feng <1304903146@qq.com>
2023-11-14 17:03:20 +08:00
fit2bot
4e5a44bd98 fix: 账号收集通知 同步资产时 计算新增账号错误 (#12101)
Co-authored-by: feng <1304903146@qq.com>
2023-11-14 14:50:33 +08:00
fit2bot
fcce03f7bd fix: 改密记录搜索失败 (#12098)
Co-authored-by: feng <1304903146@qq.com>
2023-11-14 12:48:02 +08:00
fit2bot
5f121934a7 perf: 交换机切换至卡住 (#12096)
Co-authored-by: feng <1304903146@qq.com>
2023-11-14 10:58:57 +08:00
fit2bot
521c1f0dfa perf: 修改授权动作翻译 (#12095)
Co-authored-by: feng <1304903146@qq.com>
2023-11-14 10:41:00 +08:00
ibuler
5673698a57 perf: 修改账号选择 2023-11-14 10:18:24 +08:00
fit2bot
d6b75ac700 perf: 修改默认 ansible_python_interpreter (#12093)
Co-authored-by: feng <1304903146@qq.com>
2023-11-13 18:09:09 +08:00
fit2bot
0ee14e6d85 perf: 修改翻译 (#12092)
Co-authored-by: feng <1304903146@qq.com>
2023-11-13 17:50:10 +08:00
wangruidong
9babe977d8 fix: 修改sftp账号备份文件名及任务日志提示 2023-11-13 17:05:21 +08:00
fit2bot
0f9223331c perf: 修改 m2m json filter (#12087)
* perf: 修改 m2m json filter

* perf: 修复 json 过滤问题

---------

Co-authored-by: ibuler <ibuler@qq.com>
2023-11-13 15:04:27 +08:00
fit2bot
f8a4a0e108 fix: 修复UserOtpDisableView 视图函数获取模版错误 (#12084)
Co-authored-by: feng <1304903146@qq.com>
2023-11-10 17:59:28 +08:00
ibuler
ba76f30af9 perf: 修改 applet option 2023-11-10 17:18:40 +08:00
Eric_Lee
e5e0c841a2 Revert "perf: 调整 secret 长度为32位"
This reverts commit c41fdf1786.
2023-11-10 15:27:57 +08:00
Eric
c41fdf1786 perf: 调整 secret 长度为32位 2023-11-10 15:03:51 +08:00
fit2bot
69c0eb2f50 fix: 修复平台自动化翻译 (#12078)
Co-authored-by: feng <1304903146@qq.com>
2023-11-09 17:25:32 +08:00
Bryan
e077afe2cc Update README.md 2023-11-09 14:53:49 +08:00
wangruidong
c1f572df05 fix: 【账号备份】创建账号备份存储,选择SFTP,发送服务器为空。修改执行任务的日志提示 2023-11-09 14:22:04 +08:00
fit2bot
d60fe464ca fix:修复es6.8查询不到数据问题 (#12069)
Co-authored-by: feng <1304903146@qq.com>
2023-11-09 14:18:49 +08:00
fit2bot
f47895b8a8 perf: 优化仪表盘查询sftp数量sql (#12075)
Co-authored-by: feng <1304903146@qq.com>
2023-11-09 14:16:41 +08:00
Eric
3eb1583c69 perf: 增加分享权限位 2023-11-08 19:05:51 +08:00
feng
5ab8ff4fde perf: 在线用户根据websocket添加用户是否活跃状态 2023-11-08 17:02:47 +08:00
feng
7746491e19 perf: 在线用户添加是否活跃的属性 2023-11-08 17:02:47 +08:00
Eric
5e54792d94 perf: 优化发布机终端名称 2023-11-08 13:53:24 +08:00
Eric
621c7a31fe fix: 修复发布机名称因含特殊字符部署失败的问题 2023-11-08 13:26:04 +08:00
fit2bot
75bab70ccf fix: 账号迁移文件 (#12059)
Co-authored-by: feng <1304903146@qq.com>
2023-11-08 10:33:49 +08:00
halo
30683ed859 perf: 优化连接信息超长,客户端拉起无响应问题 2023-11-07 15:47:22 +08:00
Bai
7c52cec5fb perf: Upgrade requements jms-storage-sdk==0.0.53 2023-11-07 15:46:48 +08:00
fit2bot
f01bfc44b8 perf: 账号备份增加sftp方式 (#12032)
* perf: 添加sftp支持

* perf: 账号备份增加sftp方式

---------

Co-authored-by: wangruidong <940853815@qq.com>
Co-authored-by: Bryan <jiangjie.bai@fit2cloud.com>
2023-11-07 15:10:46 +08:00
fit2bot
54b89f6fee feat: 账号收集添加资产账号信息变化通知 (#12009)
Co-authored-by: feng <1304903146@qq.com>
2023-11-07 13:00:09 +08:00
Bai
c0de0b0d8e fix: Remove repetition code 2023-11-07 11:30:53 +08:00
huailei
06275a09ac Merge pull request #12042 from jumpserver/pr@dev@ansible
perf: 密码中支持特殊字符比如"
2023-11-06 18:19:34 +08:00
feng
7b86938b58 perf: 密码中支持特殊字符比如" 2023-11-06 17:53:18 +08:00
fit2bot
44624d0ce0 feat: 工作台支持配置显示系统工具 (#12013)
Co-authored-by: halo <wuyihuangw@gmail.com>
2023-11-03 17:33:44 +08:00
wangruidong
9b8c817a16 perf: 修改字段翻译 2023-11-03 10:45:17 +08:00
ibuler
927fe1f128 perf: 修改资产协议 xpack 2023-11-03 10:43:34 +08:00
fit2bot
eee119eba1 feat: 个人设置 rdp smart size可配置 (#12021)
Co-authored-by: feng <1304903146@qq.com>
2023-11-02 18:51:17 +08:00
老广
53d8f716eb Merge pull request #12007 from jumpserver/pr@dev@json_field_support_m2m_all
perf: JSONManyToMany 中的 m2m 方式支持包含所有
2023-11-02 10:35:28 +08:00
吴小白
f48aec2bcb Merge pull request #12011 from jumpserver/pr@dev@perf_tinker_chrome
perf: 更新 chrome 和 chromedriver
2023-11-01 20:34:53 +08:00
吴小白
78e9f51786 perf: 移除旧版本 Chrome 文件 2023-11-01 18:49:38 +08:00
吴小白
af33ad6631 perf: 移除 python3 环境变量 2023-11-01 18:35:10 +08:00
吴小白
864da49ae6 perf: 更新 chrome 和 chromedriver 2023-11-01 18:10:03 +08:00
huailei
e6b8b3982d Merge pull request #12010 from jumpserver/pr@dev@perf_mobile_login
perf: 优化登录页样式
2023-11-01 17:02:28 +08:00
“huailei000”
49b3df218e perf: 优化登录页样式 2023-11-01 17:01:14 +08:00
ibuler
0858d67098 fix: 修改可能迁移的问题 2023-11-01 03:11:47 -05:00
ibuler
ffa242e635 perf: JSONManyToMany 中的 m2m 方式支持包含所有 2023-11-01 15:38:03 +08:00
wangruidong
4021b1955e fix: 组件启动失败 2023-10-31 19:18:35 +08:00
Bryan
204258f058 Update README.md 2023-10-31 18:20:01 +08:00
wangruidong
dc841650cf perf: AKSK添加访问IP控制 2023-10-31 02:43:33 -05:00
feng
bc54685a31 feat: 改密记录 推送记录可单独执行 2023-10-31 00:57:47 -05:00
ibuler
ee586954f8 feat: 发布机支持使用同名账号连接 2023-10-31 10:18:30 +08:00
ibuler
e56a37afd2 fix: 优化选择发布机 2023-10-30 16:07:02 +08:00
老广
7669744312 Merge pull request #11981 from jumpserver/pr@dev@feat_perm_add_protocols
perf: 资产授权添加协议
2023-10-30 10:12:45 +08:00
ibuler
ad8aba88a3 perf: 资产授权添加协议 2023-10-30 10:11:36 +08:00
wangruidong
7659846df4 perf: 兼容SERVER_NAME值多种情况 2023-10-27 16:45:42 +08:00
ibuler
f93979eb2d perf: 资产授权添加协议 2023-10-27 16:15:59 +08:00
fit2bot
badf83c560 perf: 命令存储为本地数据库时 搜索资产时支持模糊搜索 (#11978)
Co-authored-by: feng <1304903146@qq.com>
2023-10-26 17:10:27 +08:00
halo
f6466a3a20 fix: 修复DB2平台已经存在的问题 2023-10-26 01:25:47 -05:00
ibuler
996394ba29 perf: 优化 profile field 2023-10-25 05:09:15 -05:00
fit2bot
09f8470d34 fix: 改密校验可连接性失败 (#11964)
Co-authored-by: feng <1304903146@qq.com>
2023-10-25 16:21:45 +08:00
Bai
fdb3f6409c fix: 修复登录日志和在线用户会话的 IP 地址获取方式 2023-10-25 01:40:16 -05:00
ibuler
73b0b23910 perf: 修改rsa key 默认长度 2023-10-25 10:05:58 +08:00
ibuler
c1185e989a perf: 修复资产类型的 bug 2023-10-24 16:19:08 +08:00
fit2bot
1239082649 fix: change secret perm 没有生成 (#11948)
Co-authored-by: feng <1304903146@qq.com>
2023-10-24 14:07:07 +08:00
fit2bot
ff073185f1 fix: 改密切换至检测可连接性 失败 (#11946)
Co-authored-by: feng <1304903146@qq.com>
2023-10-24 11:30:26 +08:00
老广
d7a682b462 Merge pull request #11945 from jumpserver/pr@dev@perf_oauth2_access_token_content_type
perf: 优化OAuth2.0获取Access_token的content_type
2023-10-24 11:29:10 +08:00
Eric_Lee
4df2bdd9b6 Merge pull request #11944 from jumpserver/pr@dev@upgrade_tinker_python
perf: 更新 tinker python 版本
2023-10-24 10:39:36 +08:00
吴小白
2437072768 perf: 清理旧版本 chromedriver PATH 2023-10-24 10:29:14 +08:00
jiangweidong
08a2d96213 perf: 优化OAuth2.0获取Access_token的content_type 2023-10-24 10:26:38 +08:00
吴小白
de7d7b41c0 perf: 更新 tinker python 版本 2023-10-24 08:46:17 +08:00
jiangweidong
b04c7f022f perf: 使用scan命令扫描在线用户 2023-10-23 04:34:12 -05:00
feng
bf0d9f4b80 fix: 删除错误的改密权限 2023-10-23 04:32:00 -05:00
wangruidong
314257f790 perf: 作业中心执行历史增加保留天数配置 2023-10-23 04:13:35 -05:00
ibuler
6d2a62e413 fix: 优化替换 DOMAINS 中端口 的问题 2023-10-22 22:32:04 -05:00
老广
1734ddc2bd Merge pull request #11926 from jumpserver/pr@dev@database_list
fix: 资产数据库 不分页时list接口错误
2023-10-20 03:51:07 -05:00
feng
7c796e8201 fix: 资产数据库 不分页时list接口错误 2023-10-20 16:35:39 +08:00
老广
62a74418ea Merge pull request #11852 from jumpserver/pr@dev@perf_core
perf: 按照需求添加 core-ce 镜像
2023-10-19 21:35:23 -05:00
fit2bot
32461078fe perf: ticket 迁移文件 (#11920)
Co-authored-by: feng <1304903146@qq.com>
2023-10-19 20:00:47 +08:00
Bai
939b517e34 fix: 修复账号改密密码规则提交不生效的问题 2023-10-19 04:30:49 -05:00
jiangweidong
66eac762ff fix: 可以清空云同步中的策略 2023-10-19 03:57:00 -05:00
吴小白
6f4082f800 fix: 修正 actions 测试构建任务 2023-10-16 14:00:40 +08:00
吴小白
edd65f965b perf: 按照需求添加 core-ce 镜像 2023-10-16 13:30:51 +08:00
169 changed files with 2887 additions and 1521 deletions

View File

@@ -19,8 +19,8 @@ jobs:
with: with:
context: . context: .
push: false push: false
tags: jumpserver/core:test tags: jumpserver/core-ce:test
file: Dockerfile file: Dockerfile-ce
build-args: | build-args: |
APT_MIRROR=http://deb.debian.org APT_MIRROR=http://deb.debian.org
PIP_MIRROR=https://pypi.org/simple PIP_MIRROR=https://pypi.org/simple

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-bullseye as stage-build FROM python:3.11-slim-bullseye as stage-1
ARG TARGETARCH ARG TARGETARCH
ARG VERSION ARG VERSION
@@ -6,9 +6,10 @@ ENV VERSION=$VERSION
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
ADD . . ADD . .
RUN cd utils && bash -ixeu build.sh RUN echo > /opt/jumpserver/config.yml \
&& cd utils && bash -ixeu build.sh
FROM python:3.11-slim-bullseye FROM python:3.11-slim-bullseye as stage-2
ARG TARGETARCH ARG TARGETARCH
ARG BUILD_DEPENDENCIES=" \ ARG BUILD_DEPENDENCIES=" \
@@ -31,6 +32,53 @@ ARG DEPENDENCIES=" \
freerdp2-dev \ freerdp2-dev \
libaio-dev" libaio-dev"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
git \
git-lfs \
unzip \
xz-utils \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install
FROM python:3.11-slim-bullseye
ARG TARGETARCH
ENV LANG=zh_CN.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libjpeg-dev \
libx11-dev \
freerdp2-dev \
libxmlsec1-openssl"
ARG TOOLS=" \ ARG TOOLS=" \
ca-certificates \ ca-certificates \
curl \ curl \
@@ -47,39 +95,30 @@ ARG TOOLS=" \
wget" wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \ && rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \ && apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \ && apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \ && mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \ && echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& sed -i "s@# export @export @g" ~/.bashrc \ && sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc \ && sed -i "s@# alias @alias @g" ~/.bashrc
&& rm -rf /var/lib/apt/lists/*
COPY --from=stage-2 /opt/py3 /opt/py3
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple ARG VERSION
RUN --mount=type=cache,target=/root/.cache \ ENV VERSION=$VERSION
set -ex \
&& echo > /opt/jumpserver/config.yml \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& poetry install --only=main
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
EXPOSE 8080 EXPOSE 8080

View File

@@ -1,9 +1,5 @@
ARG VERSION ARG VERSION
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
FROM jumpserver/core:${VERSION} FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
RUN --mount=type=cache,target=/root/.cache \
set -ex \
&& poetry install --only=xpack

View File

@@ -61,6 +61,7 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
## 案例研究 ## 案例研究
- [腾讯音乐娱乐集团基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
- [腾讯海外游戏基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704) - [腾讯海外游戏基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
- [万华化学通过JumpServer管理全球化分布式IT资产并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504) - [万华化学通过JumpServer管理全球化分布式IT资产并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
- [雪花啤酒JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412) - [雪花啤酒JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
@@ -97,7 +98,7 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
| [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 项目 | | [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 项目 |
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB | | [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 | | [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core Api 通信的组件项目 | | [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core API 通信的组件项目 |
| [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 客户端 项目 | | [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 安装包 项目 | | [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 安装包 项目 |

View File

@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import status, mixins
from rest_framework import mixins from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.tasks import execute_automation_record_task
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
@@ -29,18 +31,27 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
serializer_class = serializers.ChangeSecretRecordSerializer serializer_class = serializers.ChangeSecretRecordSerializer
filter_fields = ('asset', 'execution_id') filterset_fields = ('asset_id', 'execution_id')
search_fields = ('asset__address',) search_fields = ('asset__address',)
tp = AutomationTypes.change_secret
rbac_perms = {
'execute': 'accounts.add_changesecretexecution',
}
def get_queryset(self): def get_queryset(self):
return ChangeSecretRecord.objects.filter( return ChangeSecretRecord.objects.all()
execution__automation__type=AutomationTypes.change_secret
)
def filter_queryset(self, queryset): @action(methods=['post'], detail=False, url_path='execute')
queryset = super().filter_queryset(queryset) def execute(self, request, *args, **kwargs):
eid = self.request.query_params.get('execution_id') record_id = request.data.get('record_id')
return queryset.filter(execution_id=eid) record = self.get_queryset().filter(pk=record_id)
if not record:
return Response(
{'detail': 'record not found'},
status=status.HTTP_404_NOT_FOUND
)
task = execute_automation_record_task.delay(record_id, self.tp)
return Response({'task': task.id}, status=status.HTTP_200_OK)
class ChangSecretExecutionViewSet(AutomationExecutionViewSet): class ChangSecretExecutionViewSet(AutomationExecutionViewSet):

View File

@@ -42,6 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
class PushAccountRecordViewSet(ChangeSecretRecordViewSet): class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
serializer_class = serializers.ChangeSecretRecordSerializer serializer_class = serializers.ChangeSecretRecordSerializer
tp = AutomationTypes.push_account
def get_queryset(self): def get_queryset(self):
return ChangeSecretRecord.objects.filter( return ChangeSecretRecord.objects.filter(

View File

@@ -6,16 +6,23 @@ from django.conf import settings
from openpyxl import Workbook from openpyxl import Workbook
from rest_framework import serializers from rest_framework import serializers
from accounts.notifications import AccountBackupExecutionTaskMsg from accounts.const.automation import AccountBackupType
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
from accounts.serializers import AccountSecretSerializer from accounts.serializers import AccountSecretSerializer
from accounts.models.automations.backup_account import AccountBackupAutomation
from assets.const import AllTypes from assets.const import AllTypes
from common.utils.file import encrypt_and_compress_zip_file from common.utils.file import encrypt_and_compress_zip_file, zip_files
from common.utils.timezone import local_now_display from common.utils.timezone import local_now_filename, local_now_display
from terminal.models.component.storage import ReplayStorage
from users.models import User from users.models import User
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
class RecipientsNotFound(Exception):
pass
class BaseAccountHandler: class BaseAccountHandler:
@classmethod @classmethod
def unpack_data(cls, serializer_data, data=None): def unpack_data(cls, serializer_data, data=None):
@@ -67,7 +74,7 @@ class AssetAccountHandler(BaseAccountHandler):
@staticmethod @staticmethod
def get_filename(plan_name): def get_filename(plan_name):
filename = os.path.join( filename = os.path.join(
PATH, f'{plan_name}-{local_now_display()}-{time.time()}.xlsx' PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.xlsx'
) )
return filename return filename
@@ -143,7 +150,7 @@ class AccountBackupHandler:
wb.save(filename) wb.save(filename)
files.append(filename) files.append(filename)
timedelta = round((time.time() - time_start), 2) timedelta = round((time.time() - time_start), 2)
print('步骤完成: 用时 {}s'.format(timedelta)) print('创建备份文件完成: 用时 {}s'.format(timedelta))
return files return files
def send_backup_mail(self, files, recipients): def send_backup_mail(self, files, recipients):
@@ -152,7 +159,7 @@ class AccountBackupHandler:
recipients = User.objects.filter(id__in=list(recipients)) recipients = User.objects.filter(id__in=list(recipients))
print( print(
'\n' '\n'
'\033[32m>>> 发送备份邮件\033[0m' '\033[32m>>> 开始发送备份邮件\033[0m'
'' ''
) )
plan_name = self.plan_name plan_name = self.plan_name
@@ -161,7 +168,7 @@ class AccountBackupHandler:
attachment_list = [] attachment_list = []
else: else:
password = user.secret_key.encode('utf8') password = user.secret_key.encode('utf8')
attachment = os.path.join(PATH, f'{plan_name}-{local_now_display()}-{time.time()}.zip') attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
encrypt_and_compress_zip_file(attachment, password, files) encrypt_and_compress_zip_file(attachment, password, files)
attachment_list = [attachment, ] attachment_list = [attachment, ]
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list) AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
@@ -169,11 +176,35 @@ class AccountBackupHandler:
for file in files: for file in files:
os.remove(file) os.remove(file)
def send_backup_obj_storage(self, files, recipients, password):
if not files:
return
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
print(
'\n'
'\033[32m>>> 开始发送备份文件到sftp服务器\033[0m'
''
)
plan_name = self.plan_name
for rec in recipients:
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
if password:
print('\033[32m>>> 使用加密密码对文件进行加密中\033[0m')
password = password.encode('utf8')
encrypt_and_compress_zip_file(attachment, password, files)
else:
zip_files(attachment, files)
attachment_list = attachment
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list)
print('备份文件将发送至{}({})'.format(rec.name, rec.id))
for file in files:
os.remove(file)
def step_perform_task_update(self, is_success, reason): def step_perform_task_update(self, is_success, reason):
self.execution.reason = reason[:1024] self.execution.reason = reason[:1024]
self.execution.is_success = is_success self.execution.is_success = is_success
self.execution.save() self.execution.save()
print('已完成对任务状态的更新') print('\n已完成对任务状态的更新\n')
@staticmethod @staticmethod
def step_finished(is_success): def step_finished(is_success):
@@ -186,24 +217,11 @@ class AccountBackupHandler:
is_success = False is_success = False
error = '-' error = '-'
try: try:
recipients_part_one = self.execution.snapshot.get('recipients_part_one', []) backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value)
recipients_part_two = self.execution.snapshot.get('recipients_part_two', []) if backup_type == AccountBackupType.email.value:
if not recipients_part_one and not recipients_part_two: self.backup_by_email()
print( elif backup_type == AccountBackupType.object_storage.value:
'\n' self.backup_by_obj_storage()
'\033[32m>>> 该备份任务未分配收件人\033[0m'
''
)
if recipients_part_one and recipients_part_two:
files = self.create_excel(section='front')
self.send_backup_mail(files, recipients_part_one)
files = self.create_excel(section='back')
self.send_backup_mail(files, recipients_part_two)
else:
recipients = recipients_part_one or recipients_part_two
files = self.create_excel()
self.send_backup_mail(files, recipients)
except Exception as e: except Exception as e:
self.is_frozen = True self.is_frozen = True
print('任务执行被异常中断') print('任务执行被异常中断')
@@ -217,6 +235,52 @@ class AccountBackupHandler:
self.step_perform_task_update(is_success, reason) self.step_perform_task_update(is_success, reason)
self.step_finished(is_success) self.step_finished(is_success)
def backup_by_obj_storage(self):
object_id = self.execution.snapshot.get('id')
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
if not obj_recipients_part_one and not obj_recipients_part_two:
print(
'\n'
'\033[31m>>> 该备份任务未分配sftp服务器\033[0m'
''
)
raise RecipientsNotFound('Not Found Recipients')
if obj_recipients_part_one and obj_recipients_part_two:
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
files = self.create_excel(section='front')
self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password)
files = self.create_excel(section='back')
self.send_backup_obj_storage(files, obj_recipients_part_two, zip_encrypt_password)
else:
recipients = obj_recipients_part_one or obj_recipients_part_two
files = self.create_excel()
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
def backup_by_email(self):
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
if not recipients_part_one and not recipients_part_two:
print(
'\n'
'\033[31m>>> 该备份任务未分配收件人\033[0m'
''
)
raise RecipientsNotFound('Not Found Recipients')
if recipients_part_one and recipients_part_two:
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
files = self.create_excel(section='front')
self.send_backup_mail(files, recipients_part_one)
files = self.create_excel(section='back')
self.send_backup_mail(files, recipients_part_two)
else:
recipients = recipients_part_one or recipients_part_two
files = self.create_excel()
self.send_backup_mail(files, recipients)
def run(self): def run(self):
print('任务开始: {}'.format(local_now_display())) print('任务开始: {}'.format(local_now_display()))
time_start = time.time() time_start = time.time()
@@ -229,4 +293,4 @@ class AccountBackupHandler:
finally: finally:
print('\n任务结束: {}'.format(local_now_display())) print('\n任务结束: {}'.format(local_now_display()))
timedelta = round((time.time() - time_start), 2) timedelta = round((time.time() - time_start), 2)
print('用时: {}'.format(timedelta)) print('用时: {}s'.format(timedelta))

View File

@@ -1,6 +1,7 @@
- hosts: custom - hosts: custom
gather_facts: no gather_facts: no
vars: vars:
asset_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
ansible_connection: local ansible_connection: local
ansible_become: false ansible_become: false
@@ -8,7 +9,7 @@
- name: Test privileged account (paramiko) - name: Test privileged account (paramiko)
ssh_ping: ssh_ping:
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ asset_port }}"
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
@@ -19,13 +20,14 @@
become_password: "{{ custom_become_password | default('') }}" become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}" become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
register: ping_info register: ping_info
delegate_to: localhost
- name: Change asset password (paramiko) - name: Change asset password (paramiko)
custom_command: custom_command:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ asset_port }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}" become: "{{ custom_become | default(False) }}"
@@ -40,15 +42,17 @@
ignore_errors: true ignore_errors: true
when: ping_info is succeeded when: ping_info is succeeded
register: change_info register: change_info
delegate_to: localhost
- name: Verify password (paramiko) - name: Verify password (paramiko)
ssh_ping: ssh_ping:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ asset_port }}"
become: "{{ account.become.ansible_become | default(False) }}" become: "{{ account.become.ansible_become | default(False) }}"
become_method: su become_method: su
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
delegate_to: localhost

View File

@@ -1,7 +1,7 @@
- hosts: mongodb - hosts: mongodb
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test MongoDB connection - name: Test MongoDB connection

View File

@@ -1,8 +1,9 @@
- hosts: mysql - hosts: mysql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
db_name: "{{ jms_asset.spec_info.db_name }}" db_name: "{{ jms_asset.spec_info.db_name }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
tasks: tasks:
- name: Test MySQL connection - name: Test MySQL connection
@@ -11,10 +12,10 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version
register: db_info register: db_info
@@ -28,10 +29,10 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
@@ -45,8 +46,8 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -1,7 +1,7 @@
- hosts: oracle - hosts: oracle
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test Oracle connection - name: Test Oracle connection

View File

@@ -1,7 +1,7 @@
- hosts: postgre - hosts: postgre
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test PostgreSQL connection - name: Test PostgreSQL connection

View File

@@ -1,7 +1,7 @@
- hosts: sqlserver - hosts: sqlserver
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection

View File

@@ -0,0 +1,35 @@
- hosts: demo
gather_facts: no
tasks:
- name: Test privileged account
ansible.windows.win_ping:
# - name: Print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
- name: Change password
ansible.windows.win_user:
fullname: "{{ account.username}}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
password_never_expires: yes
groups: "{{ params.groups }}"
groups_action: add
update_password: always
ignore_errors: true
when: account.secret_type == "password"
- name: Refresh connection
ansible.builtin.meta: reset_connection
- name: Verify password (pyfreerdp)
rdp_ping:
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -0,0 +1,26 @@
id: change_secret_windows_rdp_verify
name: "{{ 'Windows account change secret rdp verify' | trans }}"
version: 1
method: change_secret
category: host
type:
- windows
params:
- name: groups
type: str
label: '用户组'
default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}"
i18n:
Windows account change secret rdp verify:
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密 RDP 协议测试最后的可连接性'
ja: 'Ansibleモジュールwin_userはWindowsアカウントの改密RDPプロトコルテストの最後の接続性を実行する'
en: 'Using the Ansible module win_user performs Windows account encryption RDP protocol testing for final connectivity'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'

View File

@@ -1,6 +1,5 @@
import os import os
import time import time
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from django.conf import settings from django.conf import settings
@@ -14,7 +13,7 @@ from accounts.serializers import ChangeSecretRecordBackUpSerializer
from assets.const import HostTypes from assets.const import HostTypes
from common.utils import get_logger from common.utils import get_logger
from common.utils.file import encrypt_and_compress_zip_file from common.utils.file import encrypt_and_compress_zip_file
from common.utils.timezone import local_now_display from common.utils.timezone import local_now_filename
from users.models import User from users.models import User
from ..base.manager import AccountBasePlaybookManager from ..base.manager import AccountBasePlaybookManager
from ...utils import SecretGenerator from ...utils import SecretGenerator
@@ -27,7 +26,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.method_hosts_mapper = defaultdict(list) self.record_id = self.execution.snapshot.get('record_id')
self.secret_type = self.execution.snapshot.get('secret_type') self.secret_type = self.execution.snapshot.get('secret_type')
self.secret_strategy = self.execution.snapshot.get( self.secret_strategy = self.execution.snapshot.get(
'secret_strategy', SecretStrategy.custom 'secret_strategy', SecretStrategy.custom
@@ -50,7 +49,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
if kwargs['strategy'] == SSHKeyStrategy.set_jms: if kwargs['strategy'] == SSHKeyStrategy.set_jms:
kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username) username = account.username
path = f'/{username}' if username == "root" else f'/home/{username}'
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
return kwargs return kwargs
@@ -96,17 +97,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
accounts = self.get_accounts(account) accounts = self.get_accounts(account)
if not accounts: if not accounts:
print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % ( print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % (
asset.name, self.account_ids, self.secret_type asset.name, self.account_ids, self.secret_type
)) ))
return [] return []
method_attr = getattr(automation, self.method_type() + '_method')
method_hosts = self.method_hosts_mapper[method_attr]
method_hosts = [h for h in method_hosts if h != host['name']]
inventory_hosts = []
records = [] records = []
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY: if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
print(f'Windows {asset} does not support ssh key push') print(f'Windows {asset} does not support ssh key push')
return inventory_hosts return inventory_hosts
@@ -116,13 +113,20 @@ class ChangeSecretManager(AccountBasePlaybookManager):
h = deepcopy(host) h = deepcopy(host)
secret_type = account.secret_type secret_type = account.secret_type
h['name'] += '(' + account.username + ')' h['name'] += '(' + account.username + ')'
new_secret = self.get_secret(secret_type) if self.secret_type is None:
new_secret = account.secret
else:
new_secret = self.get_secret(secret_type)
if self.record_id is None:
recorder = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret,
)
records.append(recorder)
else:
recorder = ChangeSecretRecord.objects.get(id=self.record_id)
recorder = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret,
)
records.append(recorder)
self.name_recorder_mapper[h['name']] = recorder self.name_recorder_mapper[h['name']] = recorder
private_key_path = None private_key_path = None
@@ -136,13 +140,12 @@ class ChangeSecretManager(AccountBasePlaybookManager):
'username': account.username, 'username': account.username,
'secret_type': secret_type, 'secret_type': secret_type,
'secret': new_secret, 'secret': new_secret,
'private_key_path': private_key_path 'private_key_path': private_key_path,
'become': account.get_ansible_become_auth(),
} }
if asset.platform.type == 'oracle': if asset.platform.type == 'oracle':
h['account']['mode'] = 'sysdba' if account.privileged else None h['account']['mode'] = 'sysdba' if account.privileged else None
inventory_hosts.append(h) inventory_hosts.append(h)
method_hosts.append(h['name'])
self.method_hosts_mapper[method_attr] = method_hosts
ChangeSecretRecord.objects.bulk_create(records) ChangeSecretRecord.objects.bulk_create(records)
return inventory_hosts return inventory_hosts
@@ -170,7 +173,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
recorder.save() recorder.save()
def on_runner_failed(self, runner, e): def on_runner_failed(self, runner, e):
logger.error("Change secret error: ", e) logger.error("Account error: ", e)
def check_secret(self): def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \ if self.secret_strategy == SecretStrategy.custom \
@@ -180,9 +183,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
return True return True
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
if not self.check_secret(): if self.secret_type and not self.check_secret():
return return
super().run(*args, **kwargs) super().run(*args, **kwargs)
if self.record_id:
return
recorders = self.name_recorder_mapper.values() recorders = self.name_recorder_mapper.values()
recorders = list(recorders) recorders = list(recorders)
self.send_recorder_mail(recorders) self.send_recorder_mail(recorders)
@@ -196,7 +201,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
name = self.execution.snapshot['name'] name = self.execution.snapshot['name']
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
filename = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.xlsx') filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
if not self.create_file(recorders, filename): if not self.create_file(recorders, filename):
return return
@@ -204,7 +209,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
attachments = [] attachments = []
if user.secret_key: if user.secret_key:
password = user.secret_key.encode('utf8') password = user.secret_key.encode('utf8')
attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip') attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip')
encrypt_and_compress_zip_file(attachment, password, [filename]) encrypt_and_compress_zip_file(attachment, password, [filename])
attachments = [attachment] attachments = [attachment]
ChangeSecretExecutionTaskMsg(name, user).publish(attachments) ChangeSecretExecutionTaskMsg(name, user).publish(attachments)

View File

@@ -1,7 +1,7 @@
- hosts: mongodb - hosts: mongodb
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Get info - name: Get info

View File

@@ -1,7 +1,8 @@
- hosts: mysql - hosts: mysql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
tasks: tasks:
- name: Get info - name: Get info
@@ -10,10 +11,10 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: users filter: users
register: db_info register: db_info

View File

@@ -1,7 +1,7 @@
- hosts: oralce - hosts: oralce
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Get info - name: Get info

View File

@@ -1,7 +1,7 @@
- hosts: postgresql - hosts: postgresql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Get info - name: Get info

View File

@@ -1,9 +1,14 @@
from collections import defaultdict
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.models import GatheredAccount from accounts.models import GatheredAccount
from assets.models import Asset
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
from users.models import User
from .filter import GatherAccountsFilter from .filter import GatherAccountsFilter
from ..base.manager import AccountBasePlaybookManager from ..base.manager import AccountBasePlaybookManager
from ...notifications import GatherAccountChangeMsg
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -12,6 +17,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.host_asset_mapper = {} self.host_asset_mapper = {}
self.asset_account_info = {}
self.asset_username_mapper = defaultdict(set)
self.is_sync_account = self.execution.snapshot.get('is_sync_account') self.is_sync_account = self.execution.snapshot.get('is_sync_account')
@classmethod @classmethod
@@ -26,10 +34,11 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def filter_success_result(self, tp, result): def filter_success_result(self, tp, result):
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result) result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
return result return result
@staticmethod
def generate_data(asset, result): def generate_data(self, asset, result):
data = [] data = []
for username, info in result.items(): for username, info in result.items():
self.asset_username_mapper[str(asset.id)].add(username)
d = {'asset': asset, 'username': username, 'present': True} d = {'asset': asset, 'username': username, 'present': True}
if info.get('date'): if info.get('date'):
d['date_last_login'] = info['date'] d['date_last_login'] = info['date']
@@ -38,26 +47,85 @@ class GatherAccountsManager(AccountBasePlaybookManager):
data.append(d) data.append(d)
return data return data
def update_or_create_accounts(self, asset, result): def collect_asset_account_info(self, asset, result):
data = self.generate_data(asset, result) data = self.generate_data(asset, result)
with tmp_to_org(asset.org_id): self.asset_account_info[asset] = data
gathered_accounts = []
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
for d in data:
username = d['username']
gathered_account, __ = GatheredAccount.objects.update_or_create(
defaults=d, asset=asset, username=username,
)
gathered_accounts.append(gathered_account)
if not self.is_sync_account:
return
GatheredAccount.sync_accounts(gathered_accounts)
def on_host_success(self, host, result): def on_host_success(self, host, result):
info = result.get('debug', {}).get('res', {}).get('info', {}) info = result.get('debug', {}).get('res', {}).get('info', {})
asset = self.host_asset_mapper.get(host) asset = self.host_asset_mapper.get(host)
if asset and info: if asset and info:
result = self.filter_success_result(asset.type, info) result = self.filter_success_result(asset.type, info)
self.update_or_create_accounts(asset, result) self.collect_asset_account_info(asset, result)
else: else:
logger.error("Not found info".format(host)) logger.error(f'Not found {host} info')
def update_or_create_accounts(self):
for asset, data in self.asset_account_info.items():
with tmp_to_org(asset.org_id):
gathered_accounts = []
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
for d in data:
username = d['username']
gathered_account, __ = GatheredAccount.objects.update_or_create(
defaults=d, asset=asset, username=username,
)
gathered_accounts.append(gathered_account)
if not self.is_sync_account:
return
GatheredAccount.sync_accounts(gathered_accounts)
def run(self, *args, **kwargs):
super().run(*args, **kwargs)
users, change_info = self.generate_send_users_and_change_info()
self.update_or_create_accounts()
self.send_email_if_need(users, change_info)
def generate_send_users_and_change_info(self):
recipients = self.execution.recipients
if not self.asset_username_mapper or not recipients:
return None, None
users = User.objects.filter(id__in=recipients)
if not users:
return users, None
asset_ids = self.asset_username_mapper.keys()
assets = Asset.objects.filter(id__in=asset_ids)
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
asset_id_map = {str(asset.id): asset for asset in assets}
asset_id_username = list(assets.values_list('id', 'accounts__username'))
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
system_asset_username_mapper = defaultdict(set)
for asset_id, username in asset_id_username:
system_asset_username_mapper[str(asset_id)].add(username)
change_info = {}
for asset_id, usernames in self.asset_username_mapper.items():
system_usernames = system_asset_username_mapper.get(asset_id)
if not system_usernames:
continue
add_usernames = usernames - system_usernames
remove_usernames = system_usernames - usernames
k = f'{asset_id_map[asset_id]}[{asset_id}]'
if not add_usernames and not remove_usernames:
continue
change_info[k] = {
'add_usernames': ', '.join(add_usernames),
'remove_usernames': ', '.join(remove_usernames),
}
return users, change_info
@staticmethod
def send_email_if_need(users, change_info):
if not users or not change_info:
return
for user in users:
GatherAccountChangeMsg(user, change_info).publish_async()

View File

@@ -1,7 +1,7 @@
- hosts: mongodb - hosts: mongodb
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test MongoDB connection - name: Test MongoDB connection

View File

@@ -1,8 +1,9 @@
- hosts: mysql - hosts: mysql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
db_name: "{{ jms_asset.spec_info.db_name }}" db_name: "{{ jms_asset.spec_info.db_name }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
tasks: tasks:
- name: Test MySQL connection - name: Test MySQL connection
@@ -11,10 +12,10 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version
register: db_info register: db_info
@@ -28,10 +29,10 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
@@ -45,8 +46,8 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -1,7 +1,7 @@
- hosts: oracle - hosts: oracle
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test Oracle connection - name: Test Oracle connection

View File

@@ -1,7 +1,7 @@
- hosts: postgre - hosts: postgre
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test PostgreSQL connection - name: Test PostgreSQL connection

View File

@@ -1,7 +1,7 @@
- hosts: sqlserver - hosts: sqlserver
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection

View File

@@ -0,0 +1,35 @@
- hosts: demo
gather_facts: no
tasks:
- name: Test privileged account
ansible.windows.win_ping:
# - name: Print variables
# debug:
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
- name: Push user password
ansible.windows.win_user:
fullname: "{{ account.username}}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
password_never_expires: yes
groups: "{{ params.groups }}"
groups_action: add
update_password: always
ignore_errors: true
when: account.secret_type == "password"
- name: Refresh connection
ansible.builtin.meta: reset_connection
- name: Verify password (pyfreerdp)
rdp_ping:
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password"
delegate_to: localhost

View File

@@ -0,0 +1,19 @@
id: push_account_windows_rdp_verify
name: "{{ 'Windows account push rdp verify' | trans }}"
version: 1
method: push_account
category: host
type:
- windows
params:
- name: groups
type: str
label: '用户组'
default: 'Users,Remote Desktop Users'
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
i18n:
Windows account push rdp verify:
zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送 RDP 协议测试最后的可连接性
ja: Ansibleモジュールwin_userがWindowsアカウントプッシュRDPプロトコルテストを実行する最後の接続性
en: Using the Ansible module win_user performs Windows account push RDP protocol testing for final connectivity

View File

@@ -1,7 +1,4 @@
from copy import deepcopy from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, SecretType, Connectivity
from assets.const import HostTypes
from common.utils import get_logger from common.utils import get_logger
from ..base.manager import AccountBasePlaybookManager from ..base.manager import AccountBasePlaybookManager
from ..change_secret.manager import ChangeSecretManager from ..change_secret.manager import ChangeSecretManager
@@ -10,83 +7,11 @@ logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
ansible_account_prefer = ''
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return AutomationTypes.push_account return AutomationTypes.push_account
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
host = super(ChangeSecretManager, self).host_callback(
host, asset=asset, account=account, automation=automation,
path_dir=path_dir, **kwargs
)
if host.get('error'):
return host
accounts = self.get_accounts(account)
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
msg = f'Windows {asset} does not support ssh key push'
print(msg)
return inventory_hosts
host['ssh_params'] = {}
for account in accounts:
h = deepcopy(host)
secret_type = account.secret_type
h['name'] += '(' + account.username + ')'
if self.secret_type is None:
new_secret = account.secret
else:
new_secret = self.get_secret(secret_type)
self.name_recorder_mapper[h['name']] = {
'account': account, 'new_secret': new_secret,
}
private_key_path = None
if secret_type == SecretType.SSH_KEY:
private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret)
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
h['account'] = {
'name': account.name,
'username': account.username,
'secret_type': secret_type,
'secret': new_secret,
'private_key_path': private_key_path
}
if asset.platform.type == 'oracle':
h['account']['mode'] = 'sysdba' if account.privileged else None
inventory_hosts.append(h)
return inventory_hosts
def on_host_success(self, host, result):
account_info = self.name_recorder_mapper.get(host)
if not account_info:
return
account = account_info['account']
new_secret = account_info['new_secret']
if not account:
return
account.secret = new_secret
account.save(update_fields=['secret'])
account.set_connectivity(Connectivity.OK)
def on_host_error(self, host, error, result):
pass
def on_runner_failed(self, runner, e):
logger.error("Pust account error: {}".format(e))
def run(self, *args, **kwargs):
if self.secret_type and not self.check_secret():
return
super(ChangeSecretManager, self).run(*args, **kwargs)
# @classmethod # @classmethod
# def trigger_by_asset_create(cls, asset): # def trigger_by_asset_create(cls, asset):
# automations = PushAccountAutomation.objects.filter( # automations = PushAccountAutomation.objects.filter(

View File

@@ -2,13 +2,14 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_connection: local ansible_connection: local
ansible_shell_type: sh
ansible_become: false ansible_become: false
tasks: tasks:
- name: Verify account (paramiko) - name: Verify account (paramiko)
ssh_ping: ssh_ping:
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"

View File

@@ -1,7 +1,7 @@
- hosts: mongdb - hosts: mongdb
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Verify account - name: Verify account

View File

@@ -1,7 +1,8 @@
- hosts: mysql - hosts: mysql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
tasks: tasks:
- name: Verify account - name: Verify account
@@ -10,8 +11,8 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -1,7 +1,7 @@
- hosts: oracle - hosts: oracle
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Verify account - name: Verify account

View File

@@ -1,7 +1,7 @@
- hosts: postgresql - hosts: postgresql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Verify account - name: Verify account

View File

@@ -1,7 +1,7 @@
- hosts: sqlserver - hosts: sqlserver
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Verify account - name: Verify account

View File

@@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
__all__ = [ __all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'PushAccountActionChoice', 'AccountBackupType'
] ]
@@ -95,3 +95,10 @@ class TriggerChoice(models.TextChoices, TreeChoices):
class PushAccountActionChoice(models.TextChoices): class PushAccountActionChoice(models.TextChoices):
create_and_push = 'create_and_push', _('Create and push') create_and_push = 'create_and_push', _('Create and push')
only_create = 'only_create', _('Only create') only_create = 'only_create', _('Only create')
class AccountBackupType(models.TextChoices):
"""Backup type"""
email = 'email', _('Email')
# 目前只支持sftp方式
object_storage = 'object_storage', _('SFTP')

View File

@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
'verbose_name': 'Automation execution', 'verbose_name': 'Automation execution',
'verbose_name_plural': 'Automation executions', 'verbose_name_plural': 'Automation executions',
'permissions': [('view_changesecretexecution', 'Can view change secret execution'), 'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
('add_changesecretexection', 'Can add change secret execution'), ('add_changesecretexecution', 'Can add change secret execution'),
('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'),
('add_gatheraccountsexecution', 'Can add gather accounts execution')], ('add_gatheraccountsexecution', 'Can add gather accounts execution')],
'proxy': True, 'proxy': True,
@@ -116,7 +116,7 @@ class Migration(migrations.Migration):
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')), ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
('status', models.CharField(default='pending', max_length=16)), ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')),
('error', models.TextField(blank=True, null=True, verbose_name='Error')), ('error', models.TextField(blank=True, null=True, verbose_name='Error')),
('account', ('account',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),
@@ -184,7 +184,7 @@ class Migration(migrations.Migration):
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='automationexecution', name='automationexecution',
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
('add_changesecretexection', 'Can add change secret execution'), ('add_changesecretexecution', 'Can add change secret execution'),
('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'),
('add_gatheraccountsexecution', 'Can add gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution'),
('view_pushaccountexecution', 'Can view push account execution'), ('view_pushaccountexecution', 'Can view push account execution'),

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.1.10 on 2023-10-24 05:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0016_accounttemplate_password_rules'),
]
operations = [
migrations.AlterModelOptions(
name='automationexecution',
options={
'permissions': [
('view_changesecretexecution', 'Can view change secret execution'),
('add_changesecretexecution', 'Can add change secret execution'),
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
('view_pushaccountexecution', 'Can view push account execution'),
('add_pushaccountexecution', 'Can add push account execution')
],
'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'},
),
]

View File

@@ -0,0 +1,45 @@
# Generated by Django 4.1.10 on 2023-11-03 07:10
import common.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0067_alter_replaystorage_type'),
('accounts', '0017_alter_automationexecution_options'),
]
operations = [
migrations.AddField(
model_name='accountbackupautomation',
name='backup_type',
field=models.CharField(choices=[('email', 'Email'), ('object_storage', 'Object Storage')], default='email', max_length=128),
),
migrations.AddField(
model_name='accountbackupautomation',
name='is_password_divided_by_email',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='accountbackupautomation',
name='is_password_divided_by_obj_storage',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='accountbackupautomation',
name='obj_recipients_part_one',
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_one_plans', to='terminal.replaystorage', verbose_name='Object Storage Recipient part one'),
),
migrations.AddField(
model_name='accountbackupautomation',
name='obj_recipients_part_two',
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_two_plans', to='terminal.replaystorage', verbose_name='Object Storage Recipient part two'),
),
migrations.AddField(
model_name='accountbackupautomation',
name='zip_encrypt_password',
field=common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True, verbose_name='Zip Encrypt Password'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.1.10 on 2023-10-31 06:12
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0018_accountbackupautomation_backup_type_and_more'),
]
operations = [
migrations.AddField(
model_name='gatheraccountsautomation',
name='recipients',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.1.10 on 2023-11-16 02:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0019_gatheraccountsautomation_recipients'),
]
operations = [
migrations.AlterField(
model_name='accountbackupautomation',
name='backup_type',
field=models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email', max_length=128, verbose_name='Backup Type'),
),
migrations.AlterField(
model_name='accountbackupautomation',
name='is_password_divided_by_email',
field=models.BooleanField(default=True, verbose_name='Is Password Divided'),
),
migrations.AlterField(
model_name='accountbackupautomation',
name='is_password_divided_by_obj_storage',
field=models.BooleanField(default=True, verbose_name='Is Password Divided'),
),
]

View File

@@ -114,7 +114,7 @@ class Account(AbsConnectivity, BaseAccount):
return auth return auth
auth.update(self.make_account_ansible_vars(su_from)) auth.update(self.make_account_ansible_vars(su_from))
become_method = 'sudo' if platform.su_method != 'su' else 'su' become_method = platform.su_method if platform.su_method else 'sudo'
password = su_from.secret if become_method == 'sudo' else self.secret password = su_from.secret if become_method == 'sudo' else self.secret
auth['ansible_become'] = True auth['ansible_become'] = True
auth['ansible_become_method'] = become_method auth['ansible_become_method'] = become_method

View File

@@ -8,10 +8,11 @@ from django.db import models
from django.db.models import F from django.db.models import F
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.const.automation import AccountBackupType
from common.const.choices import Trigger from common.const.choices import Trigger
from common.db import fields
from common.db.encoder import ModelJSONFieldEncoder from common.db.encoder import ModelJSONFieldEncoder
from common.utils import get_logger from common.utils import get_logger, lazyproperty
from common.utils import lazyproperty
from ops.mixin import PeriodTaskModelMixin from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
@@ -22,6 +23,10 @@ logger = get_logger(__file__)
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
types = models.JSONField(default=list) types = models.JSONField(default=list)
backup_type = models.CharField(max_length=128, choices=AccountBackupType.choices,
default=AccountBackupType.email.value, verbose_name=_('Backup Type'))
is_password_divided_by_email = models.BooleanField(default=True, verbose_name=_('Is Password Divided'))
is_password_divided_by_obj_storage = models.BooleanField(default=True, verbose_name=_('Is Password Divided'))
recipients_part_one = models.ManyToManyField( recipients_part_one = models.ManyToManyField(
'users.User', related_name='recipient_part_one_plans', blank=True, 'users.User', related_name='recipient_part_one_plans', blank=True,
verbose_name=_("Recipient part one") verbose_name=_("Recipient part one")
@@ -30,6 +35,16 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
'users.User', related_name='recipient_part_two_plans', blank=True, 'users.User', related_name='recipient_part_two_plans', blank=True,
verbose_name=_("Recipient part two") verbose_name=_("Recipient part two")
) )
obj_recipients_part_one = models.ManyToManyField(
'terminal.ReplayStorage', related_name='obj_recipient_part_one_plans', blank=True,
verbose_name=_("Object Storage Recipient part one")
)
obj_recipients_part_two = models.ManyToManyField(
'terminal.ReplayStorage', related_name='obj_recipient_part_two_plans', blank=True,
verbose_name=_("Object Storage Recipient part two")
)
zip_encrypt_password = fields.EncryptCharField(max_length=4096, blank=True, null=True,
verbose_name=_('Zip Encrypt Password'))
def __str__(self): def __str__(self):
return f'{self.name}({self.org_id})' return f'{self.name}({self.org_id})'
@@ -49,6 +64,7 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
def to_attr_json(self): def to_attr_json(self):
return { return {
'id': self.id,
'name': self.name, 'name': self.name,
'is_periodic': self.is_periodic, 'is_periodic': self.is_periodic,
'interval': self.interval, 'interval': self.interval,
@@ -56,6 +72,10 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
'org_id': self.org_id, 'org_id': self.org_id,
'created_by': self.created_by, 'created_by': self.created_by,
'types': self.types, 'types': self.types,
'backup_type': self.backup_type,
'is_password_divided_by_email': self.is_password_divided_by_email,
'is_password_divided_by_obj_storage': self.is_password_divided_by_obj_storage,
'zip_encrypt_password': self.zip_encrypt_password,
'recipients_part_one': { 'recipients_part_one': {
str(user.id): (str(user), bool(user.secret_key)) str(user.id): (str(user), bool(user.secret_key))
for user in self.recipients_part_one.all() for user in self.recipients_part_one.all()
@@ -63,7 +83,15 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
'recipients_part_two': { 'recipients_part_two': {
str(user.id): (str(user), bool(user.secret_key)) str(user.id): (str(user), bool(user.secret_key))
for user in self.recipients_part_two.all() for user in self.recipients_part_two.all()
} },
'obj_recipients_part_one': {
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
for obj_storage in self.obj_recipients_part_one.all()
},
'obj_recipients_part_two': {
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
for obj_storage in self.obj_recipients_part_two.all()
},
} }
@property @property

View File

@@ -33,7 +33,7 @@ class AutomationExecution(AssetAutomationExecution):
verbose_name_plural = _("Automation executions") verbose_name_plural = _("Automation executions")
permissions = [ permissions = [
('view_changesecretexecution', _('Can view change secret execution')), ('view_changesecretexecution', _('Can view change secret execution')),
('add_changesecretexection', _('Can add change secret execution')), ('add_changesecretexecution', _('Can add change secret execution')),
('view_gatheraccountsexecution', _('Can view gather accounts execution')), ('view_gatheraccountsexecution', _('Can view gather accounts execution')),
('add_gatheraccountsexecution', _('Can add gather accounts execution')), ('add_gatheraccountsexecution', _('Can add gather accounts execution')),

View File

@@ -40,7 +40,7 @@ class ChangeSecretRecord(JMSBaseModel):
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret')) new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started')) date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished')) date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
status = models.CharField(max_length=16, default='pending') status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
error = models.TextField(blank=True, null=True, verbose_name=_('Error')) error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
class Meta: class Meta:
@@ -49,9 +49,3 @@ class ChangeSecretRecord(JMSBaseModel):
def __str__(self): def __str__(self):
return self.account.__str__() return self.account.__str__()
@property
def timedelta(self):
if self.date_started and self.date_finished:
return self.date_finished - self.date_started
return None

View File

@@ -55,11 +55,15 @@ class GatherAccountsAutomation(AccountBaseAutomation):
is_sync_account = models.BooleanField( is_sync_account = models.BooleanField(
default=False, blank=True, verbose_name=_("Is sync account") default=False, blank=True, verbose_name=_("Is sync account")
) )
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
def to_attr_json(self): def to_attr_json(self):
attr_json = super().to_attr_json() attr_json = super().to_attr_json()
attr_json.update({ attr_json.update({
'is_sync_account': self.is_sync_account, 'is_sync_account': self.is_sync_account,
'recipients': [
str(recipient.id) for recipient in self.recipients.all()
]
}) })
return attr_json return attr_json

View File

@@ -1,7 +1,10 @@
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.tasks import send_mail_attachment_async from common.tasks import send_mail_attachment, upload_backup_to_obj_storage
from notifications.notifications import UserMessage
from users.models import User from users.models import User
from terminal.models.component.storage import ReplayStorage
class AccountBackupExecutionTaskMsg(object): class AccountBackupExecutionTaskMsg(object):
@@ -24,11 +27,30 @@ class AccountBackupExecutionTaskMsg(object):
"to set the encryption password").format(name) "to set the encryption password").format(name)
def publish(self, attachment_list=None): def publish(self, attachment_list=None):
send_mail_attachment_async( send_mail_attachment(
self.subject, self.message, [self.user.email], attachment_list self.subject, self.message, [self.user.email], attachment_list
) )
class AccountBackupByObjStorageExecutionTaskMsg(object):
subject = _('Notification of account backup route task results')
def __init__(self, name: str, obj_storage: ReplayStorage):
self.name = name
self.obj_storage = obj_storage
@property
def message(self):
name = self.name
return _('{} - The account backup passage task has been completed.'
' See the attachment for details').format(name)
def publish(self, attachment_list=None):
upload_backup_to_obj_storage(
self.obj_storage, attachment_list
)
class ChangeSecretExecutionTaskMsg(object): class ChangeSecretExecutionTaskMsg(object):
subject = _('Notification of implementation result of encryption change plan') subject = _('Notification of implementation result of encryption change plan')
@@ -48,6 +70,28 @@ class ChangeSecretExecutionTaskMsg(object):
"file encryption password to set the encryption password").format(name) "file encryption password to set the encryption password").format(name)
def publish(self, attachments=None): def publish(self, attachments=None):
send_mail_attachment_async( send_mail_attachment(
self.subject, self.message, [self.user.email], attachments self.subject, self.message, [self.user.email], attachments
) )
class GatherAccountChangeMsg(UserMessage):
subject = _('Gather account change information')
def __init__(self, user, change_info: dict):
self.change_info = change_info
super().__init__(user)
def get_html_msg(self) -> dict:
context = {'change_info': self.change_info}
message = render_to_string('accounts/asset_account_change_info.html', context)
return {
'subject': str(self.subject),
'message': message
}
@classmethod
def gen_test_msg(cls):
user = User.objects.first()
return cls(user, {})

View File

@@ -5,7 +5,7 @@ from rest_framework import serializers
from accounts.models import AccountBackupAutomation, AccountBackupExecution from accounts.models import AccountBackupAutomation, AccountBackupExecution
from common.const.choices import Trigger from common.const.choices import Trigger
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField, EncryptedField
from common.utils import get_logger from common.utils import get_logger
from ops.mixin import PeriodTaskSerializerMixin from ops.mixin import PeriodTaskSerializerMixin
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -16,6 +16,11 @@ __all__ = ['AccountBackupSerializer', 'AccountBackupPlanExecutionSerializer']
class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
zip_encrypt_password = EncryptedField(
label=_('Zip Encrypt Password'), required=False, max_length=40960, allow_blank=True,
allow_null=True, write_only=True,
)
class Meta: class Meta:
model = AccountBackupAutomation model = AccountBackupAutomation
read_only_fields = [ read_only_fields = [
@@ -24,7 +29,9 @@ class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSer
] ]
fields = read_only_fields + [ fields = read_only_fields + [
'id', 'name', 'is_periodic', 'interval', 'crontab', 'id', 'name', 'is_periodic', 'interval', 'crontab',
'comment', 'types', 'recipients_part_one', 'recipients_part_two' 'comment', 'types', 'recipients_part_one', 'recipients_part_two', 'backup_type',
'is_password_divided_by_email', 'is_password_divided_by_obj_storage', 'obj_recipients_part_one',
'obj_recipients_part_two', 'zip_encrypt_password'
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': True}, 'name': {'required': True},

View File

@@ -71,7 +71,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
return password_rules return password_rules
length = password_rules.get('length') length = password_rules.get('length')
symbol_set = password_rules.get('symbol_set', '')
try: try:
length = int(length) length = int(length)
@@ -84,10 +83,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
msg = _('* Password length range 6-30 bits') msg = _('* Password length range 6-30 bits')
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
if not isinstance(symbol_set, str):
symbol_set = str(symbol_set)
password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
return password_rules return password_rules
def validate(self, attrs): def validate(self, attrs):
@@ -117,8 +112,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ChangeSecretRecord model = ChangeSecretRecord
fields = [ fields = [
'id', 'asset', 'account', 'date_started', 'date_finished', 'id', 'asset', 'account', 'date_finished',
'timedelta', 'is_success', 'error', 'execution', 'status', 'is_success', 'error', 'execution',
] ]
read_only_fields = fields read_only_fields = fields

View File

@@ -18,7 +18,7 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
model = GatherAccountsAutomation model = GatherAccountsAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields \ fields = BaseAutomationSerializer.Meta.fields \
+ ['is_sync_account'] + read_only_fields + ['is_sync_account', 'recipients'] + read_only_fields
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs

View File

@@ -1,7 +1,8 @@
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _, gettext_noop
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.tasks.common import quickstart_automation_by_snapshot
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
@@ -33,3 +34,39 @@ def execute_account_automation_task(pid, trigger, tp):
return return
with tmp_to_org(instance.org): with tmp_to_org(instance.org):
instance.execute(trigger) instance.execute(trigger)
def record_task_activity_callback(self, record_id, *args, **kwargs):
from accounts.models import ChangeSecretRecord
with tmp_to_root_org():
record = get_object_or_none(ChangeSecretRecord, id=record_id)
if not record:
return
resource_ids = [record.id]
org_id = record.execution.org_id
return resource_ids, org_id
@shared_task(
queue='ansible', verbose_name=_('Execute automation record'),
activity_callback=record_task_activity_callback
)
def execute_automation_record_task(record_id, tp):
from accounts.models import ChangeSecretRecord
with tmp_to_root_org():
instance = get_object_or_none(ChangeSecretRecord, pk=record_id)
if not instance:
logger.error("No automation record found: {}".format(record_id))
return
task_name = gettext_noop('Execute automation record')
task_snapshot = {
'secret': instance.new_secret,
'secret_type': instance.execution.snapshot.get('secret_type'),
'accounts': [str(instance.account_id)],
'assets': [str(instance.asset_id)],
'params': {},
'record_id': record_id,
}
with tmp_to_org(instance.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)

View File

@@ -0,0 +1,20 @@
{% load i18n %}
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
<caption></caption>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 10px; font-weight: bold;">{% trans 'Asset' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Added account' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
</tr>
{% for name, change in change_info.items %}
<tr style="{% cycle 'background-color: #ebf5ff;' 'background-color: #fff;' %}">
<td style="border: 1px solid #ddd; padding: 10px;">{{ name }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.add_usernames }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.remove_usernames }}</td>
</tr>
{% endfor %}
</table>

View File

@@ -49,15 +49,15 @@ def validate_password_for_ansible(password):
# validate password contains left double curly bracket # validate password contains left double curly bracket
# check password not contains `{{` # check password not contains `{{`
# Ansible 推送的时候不支持 # Ansible 推送的时候不支持
if '{{' in password: if '{{' in password or '}}' in password:
raise serializers.ValidationError(_('Password can not contains `{{` ')) raise serializers.ValidationError(_('Password can not contains `{{` or `}}`'))
if '{%' in password: if '{%' in password or '%}' in password:
raise serializers.ValidationError(_('Password can not contains `{%` ')) raise serializers.ValidationError(_('Password can not contains `{%` or `%}`'))
# Ansible Windows 推送的时候不支持 # Ansible Windows 推送的时候不支持
if "'" in password: # if "'" in password:
raise serializers.ValidationError(_("Password can not contains `'` ")) # raise serializers.ValidationError(_("Password can not contains `'` "))
if '"' in password: # if '"' in password:
raise serializers.ValidationError(_('Password can not contains `"` ')) # raise serializers.ValidationError(_('Password can not contains `"` '))
def validate_ssh_key(ssh_key, passphrase=None): def validate_ssh_key(ssh_key, passphrase=None):

View File

@@ -6,4 +6,5 @@ from .label import *
from .mixin import * from .mixin import *
from .node import * from .node import *
from .platform import * from .platform import *
from .protocol import *
from .tree import * from .tree import *

View File

@@ -13,7 +13,7 @@ __all__ = ['CategoryViewSet']
class CategoryViewSet(ListModelMixin, JMSGenericViewSet): class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
serializer_classes = { serializer_classes = {
'default': CategorySerializer, 'default': CategorySerializer,
'types': TypeSerializer 'types': TypeSerializer,
} }
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)

View File

@@ -63,6 +63,8 @@ class SerializeToTreeNodeMixin:
return AllTypes.get_types_values(exclude_custom=True) return AllTypes.get_types_values(exclude_custom=True)
def get_icon(self, asset): def get_icon(self, asset):
if asset.category == 'device':
return 'switch'
if asset.type in self.support_types: if asset.type in self.support_types:
return asset.type return asset.type
else: else:

View File

@@ -0,0 +1,15 @@
from rest_framework.generics import ListAPIView
from assets import serializers
from assets.const import Protocol
from common.permissions import IsValidUser
__all__ = ['ProtocolListApi']
class ProtocolListApi(ListAPIView):
serializer_class = serializers.ProtocolSerializer
permission_classes = (IsValidUser,)
def get_queryset(self):
return list(Protocol.protocols())

View File

@@ -1,8 +1,7 @@
import hashlib
import json import json
import os import os
import shutil import shutil
from collections import defaultdict
from hashlib import md5
from socket import gethostname from socket import gethostname
import yaml import yaml
@@ -37,8 +36,6 @@ class BasePlaybookManager:
} }
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook # 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
# 避免一个 playbook 中包含太多的主机
self.method_hosts_mapper = defaultdict(list)
self.playbooks = [] self.playbooks = []
self.gateway_servers = dict() self.gateway_servers = dict()
params = self.execution.snapshot.get('params') params = self.execution.snapshot.get('params')
@@ -146,7 +143,7 @@ class BasePlaybookManager:
@staticmethod @staticmethod
def generate_private_key_path(secret, path_dir): def generate_private_key_path(secret, path_dir):
key_name = '.' + md5(secret.encode('utf-8')).hexdigest() key_name = '.' + hashlib.md5(secret.encode('utf-8')).hexdigest()
key_path = os.path.join(path_dir, key_name) key_path = os.path.join(path_dir, key_name)
if not os.path.exists(key_path): if not os.path.exists(key_path):

View File

@@ -1,7 +1,7 @@
- hosts: mongodb - hosts: mongodb
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Get info - name: Get info

View File

@@ -1,7 +1,8 @@
- hosts: mysql - hosts: mysql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
tasks: tasks:
- name: Get info - name: Get info
@@ -10,10 +11,10 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version
register: db_info register: db_info

View File

@@ -1,7 +1,7 @@
- hosts: oracle - hosts: oracle
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Get info - name: Get info

View File

@@ -1,7 +1,7 @@
- hosts: postgresql - hosts: postgresql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Get info - name: Get info

View File

@@ -2,6 +2,7 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_connection: local ansible_connection: local
ansible_shell_type: sh
ansible_become: false ansible_become: false
tasks: tasks:
@@ -10,7 +11,7 @@
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}" become: "{{ custom_become | default(False) }}"

View File

@@ -1,7 +1,7 @@
- hosts: mongodb - hosts: mongodb
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test MongoDB connection - name: Test MongoDB connection

View File

@@ -1,7 +1,8 @@
- hosts: mysql - hosts: mysql
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
tasks: tasks:
- name: Test MySQL connection - name: Test MySQL connection
@@ -10,8 +11,8 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -1,7 +1,7 @@
- hosts: oracle - hosts: oracle
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test Oracle connection - name: Test Oracle connection

View File

@@ -1,7 +1,7 @@
- hosts: postgre - hosts: postgre
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test PostgreSQL connection - name: Test PostgreSQL connection

View File

@@ -1,7 +1,7 @@
- hosts: sqlserver - hosts: sqlserver
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /usr/local/bin/python ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection

View File

@@ -6,6 +6,8 @@ logger = get_logger(__name__)
class PingManager(BasePlaybookManager): class PingManager(BasePlaybookManager):
ansible_account_prefer = ''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.host_asset_and_account_mapper = {} self.host_asset_and_account_mapper = {}

View File

@@ -64,14 +64,14 @@ class BaseType(TextChoices):
@classmethod @classmethod
def _parse_protocols(cls, protocol, tp): def _parse_protocols(cls, protocol, tp):
from .protocol import Protocol from .protocol import Protocol
settings = Protocol.settings() _settings = Protocol.settings()
choices = protocol.get('choices', []) choices = protocol.get('choices', [])
if choices == '__self__': if choices == '__self__':
choices = [tp] choices = [tp]
protocols = [] protocols = []
for name in choices: for name in choices:
protocol = {'name': name, **settings.get(name, {})} protocol = {'name': name, **_settings.get(name, {})}
setting = protocol.pop('setting', {}) setting = protocol.pop('setting', {})
setting_values = {k: v.get('default', None) for k, v in setting.items()} setting_values = {k: v.get('default', None) for k, v in setting.items()}
protocol['setting'] = setting_values protocol['setting'] = setting_values
@@ -112,7 +112,7 @@ class BaseType(TextChoices):
@classmethod @classmethod
def get_choices(cls): def get_choices(cls):
if not settings.XPACK_LICENSE_IS_VALID: if not settings.XPACK_ENABLED:
return [ return [
(tp.value, tp.label) (tp.value, tp.label)
for tp in cls.get_community_types() for tp in cls.get_community_types()

View File

@@ -27,7 +27,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
redis = 'redis', 'Redis' redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB' mongodb = 'mongodb', 'MongoDB'
k8s = 'k8s', 'K8S' k8s = 'k8s', 'K8s'
http = 'http', 'HTTP(s)' http = 'http', 'HTTP(s)'
chatgpt = 'chatgpt', 'ChatGPT' chatgpt = 'chatgpt', 'ChatGPT'
@@ -294,12 +294,22 @@ class Protocol(ChoicesMixin, models.TextChoices):
**cls.gpt_protocols(), **cls.gpt_protocols(),
} }
@classmethod
@cached_method(ttl=600)
def protocols(cls):
protocols = []
xpack_enabled = settings.XPACK_ENABLED
for protocol, config in cls.settings().items():
if not xpack_enabled and config.get('xpack', False):
continue
protocols.append(protocol)
return protocols
@classmethod @classmethod
@cached_method(ttl=600) @cached_method(ttl=600)
def xpack_protocols(cls): def xpack_protocols(cls):
return [ return [
protocol protocol for protocol, config in cls.settings().items()
for protocol, config in cls.settings().items()
if config.get('xpack', False) if config.get('xpack', False)
] ]

View File

@@ -83,7 +83,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='protocol', name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
), ),
migrations.RunPython(migrate_admin_user_to_system_user), migrations.RunPython(migrate_admin_user_to_system_user),
migrations.RenameField( migrations.RenameField(

View File

@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='protocol', name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
), ),
] ]

View File

@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='protocol', name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
), ),
migrations.CreateModel( migrations.CreateModel(
name='AccountBackupPlanExecution', name='AccountBackupPlanExecution',

View File

@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='protocol', name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
), ),
] ]

View File

@@ -107,8 +107,9 @@ def create_app_nodes(apps, org_id):
'key': next_key, 'value': name, 'parent_key': parent_key, 'key': next_key, 'value': name, 'parent_key': parent_key,
'full_value': full_value, 'org_id': org_id 'full_value': full_value, 'org_id': org_id
} }
node, created = node_model.objects.get_or_create( node, __ = node_model.objects.get_or_create(
defaults=defaults, value=name, org_id=org_id, defaults=defaults, value=name, org_id=org_id,
parent_key=parent_key
) )
node.parent = parent node.parent = parent
return node return node

View File

@@ -6,17 +6,22 @@ from django.db import migrations
def add_db2_platform(apps, schema_editor): def add_db2_platform(apps, schema_editor):
platform_cls = apps.get_model('assets', 'Platform') platform_cls = apps.get_model('assets', 'Platform')
automation_cls = apps.get_model('assets', 'PlatformAutomation') automation_cls = apps.get_model('assets', 'PlatformAutomation')
platform = platform_cls.objects.create( platform, _ = platform_cls.objects.update_or_create(
name='DB2', internal=True, category='database', type='db2', name='DB2', defaults={
domain_enabled=True, su_enabled=False, comment='DB2', 'name': 'DB2', 'category': 'database',
created_by='System', updated_by='System', 'internal': True, 'type': 'db2',
'domain_enabled': True, 'su_enabled': False,
'su_method': None, 'comment': 'DB2', 'created_by': 'System',
'updated_by': 'System', 'custom_fields': []
}
) )
platform.protocols.create(name='db2', port=5000, primary=True, setting={}) platform.protocols.update_or_create(name='db2', defaults={
automation_cls.objects.create(ansible_enabled=False, platform=platform) 'name': 'db2', 'port': 50000, 'primary': True, 'setting': {}
})
automation_cls.objects.update_or_create(platform=platform, defaults={'ansible_enabled': False})
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('assets', '0123_device_automation_ansible_enabled'), ('assets', '0123_device_automation_ansible_enabled'),
] ]

View File

@@ -131,8 +131,16 @@ class JSONFilterMixin:
value = [value] value = [value]
if name == 'nodes': if name == 'nodes':
nodes = Node.objects.filter(id__in=value) nodes = Node.objects.filter(id__in=value)
children = Node.get_nodes_all_children(nodes, with_self=True).values_list('id', flat=True) if match == 'm2m_all':
return Q(nodes__in=children) assets = Asset.objects.all()
for n in nodes:
children_pattern = Node.get_node_all_children_key_pattern(n.key)
assets = assets.filter(nodes__key__regex=children_pattern)
q = Q(id__in=assets.values_list('id', flat=True))
return q
else:
children = Node.get_nodes_all_children(nodes, with_self=True).values_list('id', flat=True)
return Q(nodes__in=children)
elif name == 'category': elif name == 'category':
return Q(platform__category__in=value) return Q(platform__category__in=value)
elif name == 'type': elif name == 'type':

View File

@@ -1,3 +1,4 @@
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@@ -34,7 +35,7 @@ class DatabaseSerializer(AssetSerializer):
if not platform_id and self.instance: if not platform_id and self.instance:
platform = self.instance.platform platform = self.instance.platform
elif getattr(self, 'instance', None): elif getattr(self, 'instance', None):
if isinstance(self.instance, list): if isinstance(self.instance, (list, QuerySet)):
return return
platform = self.instance.platform platform = self.instance.platform
elif self.context.get('request'): elif self.context.get('request'):

View File

@@ -1,5 +1,9 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
__all__ = [
'TypeSerializer', 'CategorySerializer', 'ProtocolSerializer'
]
class TypeSerializer(serializers.Serializer): class TypeSerializer(serializers.Serializer):
@@ -13,3 +17,8 @@ class CategorySerializer(serializers.Serializer):
label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label')) label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label'))
value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value')) value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value'))
types = TypeSerializer(many=True, required=False, label=_('Types'), read_only=True) types = TypeSerializer(many=True, required=False, label=_('Types'), read_only=True)
class ProtocolSerializer(serializers.Serializer):
label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label'))
value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value'))

View File

@@ -94,10 +94,17 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
setting_fields = protocol_settings.get(protocol, {}).get('setting') setting_fields = protocol_settings.get(protocol, {}).get('setting')
if not setting_fields: if not setting_fields:
return default_field return default_field
setting_fields = [{'name': k, **v} for k, v in setting_fields.items()] setting_fields = [{'name': k, **v} for k, v in setting_fields.items()]
name = '{}ProtocolSettingSerializer'.format(protocol.capitalize()) name = '{}ProtocolSettingSerializer'.format(protocol.capitalize())
return create_serializer_class(name, setting_fields)() return create_serializer_class(name, setting_fields)()
def validate(self, cleaned_data):
name = cleaned_data.get('name')
if name in ['winrm']:
cleaned_data['public'] = False
return cleaned_data
def to_file_representation(self, data): def to_file_representation(self, data):
return '{name}/{port}'.format(**data) return '{name}/{port}'.format(**data)

View File

@@ -26,6 +26,7 @@ router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-set
urlpatterns = [ urlpatterns = [
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), # path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
path('protocols/', api.ProtocolListApi.as_view(), name='asset-protocol'),
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'), path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'), path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),

View File

@@ -29,6 +29,7 @@ from terminal.models import default_storage
from users.models import User from users.models import User
from .backends import TYPE_ENGINE_MAPPING from .backends import TYPE_ENGINE_MAPPING
from .const import ActivityChoices from .const import ActivityChoices
from .filters import UserSessionFilterSet
from .models import ( from .models import (
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
ActivityLog, JobLog, UserSession ActivityLog, JobLog, UserSession
@@ -255,7 +256,7 @@ class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet): class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
http_method_names = ('get', 'post', 'head', 'options', 'trace') http_method_names = ('get', 'post', 'head', 'options', 'trace')
serializer_class = UserSessionSerializer serializer_class = UserSessionSerializer
filterset_fields = ['id', 'ip', 'city', 'type'] filterset_class = UserSessionFilterSet
search_fields = ['id', 'ip', 'city'] search_fields = ['id', 'ip', 'city']
rbac_perms = { rbac_perms = {
'offline': ['audits.offline_usersession'] 'offline': ['audits.offline_usersession']

View File

@@ -1,11 +1,12 @@
from django.db.models import F, Value from django.core.cache import cache
from django.db.models.functions import Concat from django_filters import rest_framework as drf_filters
from django_filters.rest_framework import CharFilter
from rest_framework import filters from rest_framework import filters
from rest_framework.compat import coreapi, coreschema from rest_framework.compat import coreapi, coreschema
from orgs.utils import current_org
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from notifications.ws import WS_SESSION_KEY
from orgs.utils import current_org
from .models import UserSession
__all__ = ['CurrentOrgMembersFilter'] __all__ = ['CurrentOrgMembersFilter']
@@ -34,21 +35,21 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
queryset = queryset.filter(user__in=self._get_user_list()) queryset = queryset.filter(user__in=self._get_user_list())
return queryset return queryset
#
# class CommandExecutionFilter(BaseFilterSet): class UserSessionFilterSet(BaseFilterSet):
# hostname_ip = CharFilter(method='filter_hostname_ip') is_active = drf_filters.BooleanFilter(method='filter_is_active')
#
# class Meta: @staticmethod
# model = CommandExecution.hosts.through def filter_is_active(queryset, name, is_active):
# fields = ( redis_client = cache.client.get_client()
# 'id', 'asset', 'commandexecution', 'hostname_ip' members = redis_client.smembers(WS_SESSION_KEY)
# ) members = [member.decode('utf-8') for member in members]
# if is_active:
# def filter_hostname_ip(self, queryset, name, value): queryset = queryset.filter(key__in=members)
# queryset = queryset.annotate( else:
# hostname_ip=Concat( queryset = queryset.exclude(key__in=members)
# F('asset__hostname'), Value('('), return queryset
# F('asset__address'), Value(')')
# ) class Meta:
# ).filter(hostname_ip__icontains=value) model = UserSession
# return queryset fields = ['id', 'ip', 'city', 'type']

View File

@@ -4,7 +4,7 @@ from datetime import timedelta
from importlib import import_module from importlib import import_module
from django.conf import settings from django.conf import settings
from django.core.cache import caches from django.core.cache import caches, cache
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
@@ -12,6 +12,7 @@ from django.utils.translation import gettext, gettext_lazy as _
from common.db.encoder import ModelJSONFieldEncoder from common.db.encoder import ModelJSONFieldEncoder
from common.utils import lazyproperty, i18n_trans from common.utils import lazyproperty, i18n_trans
from notifications.ws import WS_SESSION_KEY
from ops.models import JobExecution from ops.models import JobExecution
from orgs.mixins.models import OrgModelMixin, Organization from orgs.mixins.models import OrgModelMixin, Organization
from orgs.utils import current_org from orgs.utils import current_org
@@ -275,6 +276,11 @@ class UserSession(models.Model):
def backend_display(self): def backend_display(self):
return gettext(self.backend) return gettext(self.backend)
@property
def is_active(self):
redis_client = cache.client.get_client()
return redis_client.sismember(WS_SESSION_KEY, self.key)
@property @property
def date_expired(self): def date_expired(self):
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
@@ -287,7 +293,7 @@ class UserSession(models.Model):
def get_keys(): def get_keys():
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
cache_key_prefix = session_store_cls.cache_key_prefix cache_key_prefix = session_store_cls.cache_key_prefix
keys = caches[settings.SESSION_CACHE_ALIAS].keys('*') keys = caches[settings.SESSION_CACHE_ALIAS].iter_keys('*')
return [k.replace(cache_key_prefix, '') for k in keys] return [k.replace(cache_key_prefix, '') for k in keys]
@classmethod @classmethod

View File

@@ -25,10 +25,13 @@ class JobLogSerializer(JobExecutionSerializer):
read_only_fields = [ read_only_fields = [
"id", "material", "time_cost", 'date_start', "id", "material", "time_cost", 'date_start',
'date_finished', 'date_created', 'date_finished', 'date_created',
'is_finished', 'is_success', 'created_by', 'is_finished', 'is_success',
'task_id' 'task_id', 'creator_name'
] ]
fields = read_only_fields + [] fields = read_only_fields + []
extra_kwargs = {
"creator_name": {"label": _("Creator")},
}
class FTPLogSerializer(serializers.ModelSerializer): class FTPLogSerializer(serializers.ModelSerializer):
@@ -177,7 +180,7 @@ class UserSessionSerializer(serializers.ModelSerializer):
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'type', 'ip', 'city', 'user_agent', 'user', 'is_current_user_session', 'type', 'ip', 'city', 'user_agent', 'user', 'is_current_user_session',
'backend', 'backend_display', 'date_created', 'date_expired' 'backend', 'backend_display', 'is_active', 'date_created', 'date_expired'
] ]
fields = fields_small fields = fields_small
extra_kwargs = { extra_kwargs = {

View File

@@ -14,7 +14,7 @@ from acls.notifications import UserLoginReminderMsg
from audits.models import UserLoginLog from audits.models import UserLoginLog
from authentication.signals import post_auth_failed, post_auth_success from authentication.signals import post_auth_failed, post_auth_success
from authentication.utils import check_different_city_login_if_need from authentication.utils import check_different_city_login_if_need
from common.utils import get_request_ip, get_logger from common.utils import get_request_ip_or_data, get_logger
from users.models import User from users.models import User
from ..const import LoginTypeChoices from ..const import LoginTypeChoices
from ..models import UserSession from ..models import UserSession
@@ -60,7 +60,7 @@ def get_login_backend(request):
def generate_data(username, request, login_type=None): def generate_data(username, request, login_type=None):
user_agent = request.META.get('HTTP_USER_AGENT', '') user_agent = request.META.get('HTTP_USER_AGENT', '')
login_ip = get_request_ip(request) or '0.0.0.0' login_ip = get_request_ip_or_data(request) or '0.0.0.0'
if login_type is None and isinstance(request, Request): if login_type is None and isinstance(request, Request):
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U') login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')

View File

@@ -39,6 +39,7 @@ def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
log_id, before_instance = get_instance_dict_from_cache(instance_id) log_id, before_instance = get_instance_dict_from_cache(instance_id)
field_name = str(model._meta.verbose_name) field_name = str(model._meta.verbose_name)
pk_set = pk_set or {}
objs = model.objects.filter(pk__in=pk_set) objs = model.objects.filter(pk__in=pk_set)
objs_display = [str(o) for o in objs] objs_display = [str(o) for o in objs]
action = M2M_ACTION[action] action = M2M_ACTION[action]

View File

@@ -10,6 +10,7 @@ from django.core.files.storage import default_storage
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.const.crontab import CRONTAB_AT_AM_TWO
from common.utils import get_log_keep_day, get_logger from common.utils import get_log_keep_day, get_logger
from common.storage.ftp_file import FTPFileStorageHandler from common.storage.ftp_file import FTPFileStorageHandler
from ops.celery.decorator import ( from ops.celery.decorator import (
@@ -20,7 +21,6 @@ from terminal.models import Session, Command
from terminal.backends import server_replay_storage from terminal.backends import server_replay_storage
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -99,7 +99,7 @@ def clean_expired_session_period():
@shared_task(verbose_name=_('Clean audits session task log')) @shared_task(verbose_name=_('Clean audits session task log'))
@register_as_period_task(crontab='0 2 * * *') @register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
@after_app_shutdown_clean_periodic @after_app_shutdown_clean_periodic
def clean_audits_log_period(): def clean_audits_log_period():
print("Start clean audit session task log") print("Start clean audit session task log")

View File

@@ -27,6 +27,7 @@ from perms.models import ActionChoices
from terminal.connect_methods import NativeClient, ConnectMethodUtil from terminal.connect_methods import NativeClient, ConnectMethodUtil
from terminal.models import EndpointRule, Endpoint from terminal.models import EndpointRule, Endpoint
from users.const import FileNameConflictResolution from users.const import FileNameConflictResolution
from users.const import RDPSmartSize
from users.models import Preference from users.models import Preference
from ..models import ConnectionToken, date_expired_default from ..models import ConnectionToken, date_expired_default
from ..serializers import ( from ..serializers import (
@@ -66,18 +67,12 @@ class RDPFileClientProtocolURLMixin:
'autoreconnection enabled:i': '1', 'autoreconnection enabled:i': '1',
'bookmarktype:i': '3', 'bookmarktype:i': '3',
'use redirection server name:i': '0', 'use redirection server name:i': '0',
'smart sizing:i': '1',
} }
# 设置多屏显示 # 设置多屏显示
multi_mon = is_true(self.request.query_params.get('multi_mon')) multi_mon = is_true(self.request.query_params.get('multi_mon'))
if multi_mon: if multi_mon:
rdp_options['use multimon:i'] = '1' rdp_options['use multimon:i'] = '1'
# 设置多屏显示
multi_mon = is_true(self.request.query_params.get('multi_mon'))
if multi_mon:
rdp_options['use multimon:i'] = '1'
# 设置磁盘挂载 # 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect')) drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect: if drives_redirect:
@@ -106,6 +101,7 @@ class RDPFileClientProtocolURLMixin:
rdp_options['dynamic resolution:i'] = '0' rdp_options['dynamic resolution:i'] = '0'
# 设置其他选项 # 设置其他选项
rdp_options['smart sizing:i'] = self.request.query_params.get('rdp_smart_size', RDPSmartSize.DISABLE)
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0') rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
@@ -159,7 +155,7 @@ class RDPFileClientProtocolURLMixin:
account = token.account or token.input_username account = token.account or token.input_username
datetime = timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H:%M:%S') datetime = timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H:%M:%S')
name = account + '@' + str(asset) + '[' + datetime + ']' name = account + '@' + asset.name + '[' + datetime + ']'
data = { data = {
'version': 2, 'version': 2,
'id': str(token.id), # 兼容老的,未来几个版本删掉 'id': str(token.id), # 兼容老的,未来几个版本删掉
@@ -351,8 +347,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
self._insert_connect_options(data, user) self._insert_connect_options(data, user)
asset = data.get('asset') asset = data.get('asset')
account_name = data.get('account') account_name = data.get('account')
protocol = data.get('protocol')
self.input_username = self.get_input_username(data) self.input_username = self.get_input_username(data)
_data = self._validate(user, asset, account_name) _data = self._validate(user, asset, account_name, protocol)
data.update(_data) data.update(_data)
return serializer return serializer
@@ -360,12 +357,12 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
user = token.user user = token.user
asset = token.asset asset = token.asset
account_name = token.account account_name = token.account
_data = self._validate(user, asset, account_name) _data = self._validate(user, asset, account_name, token.protocol)
for k, v in _data.items(): for k, v in _data.items():
setattr(token, k, v) setattr(token, k, v)
return token return token
def _validate(self, user, asset, account_name): def _validate(self, user, asset, account_name, protocol):
data = dict() data = dict()
data['org_id'] = asset.org_id data['org_id'] = asset.org_id
data['user'] = user data['user'] = user
@@ -374,7 +371,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
if account_name == AliasAccount.ANON and asset.category not in ['web', 'custom']: if account_name == AliasAccount.ANON and asset.category not in ['web', 'custom']:
raise ValidationError(_('Anonymous account is not supported for this asset')) raise ValidationError(_('Anonymous account is not supported for this asset'))
account = self._validate_perm(user, asset, account_name) account = self._validate_perm(user, asset, account_name, protocol)
if account.has_secret: if account.has_secret:
data['input_secret'] = '' data['input_secret'] = ''
@@ -387,9 +384,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
return data return data
@staticmethod @staticmethod
def _validate_perm(user, asset, account_name): def _validate_perm(user, asset, account_name, protocol):
from perms.utils.account import PermAccountUtil from perms.utils.asset_perm import PermAssetDetailUtil
account = PermAccountUtil().validate_permission(user, asset, account_name) account = PermAssetDetailUtil(user, asset).validate_permission(account_name, protocol)
if not account or not account.actions: if not account or not account.actions:
msg = _('Account not found') msg = _('Account not found')
raise JMSException(code='perm_account_invalid', detail=msg) raise JMSException(code='perm_account_invalid', detail=msg)

View File

@@ -8,7 +8,8 @@ from django.utils.translation import gettext as _
from rest_framework import authentication, exceptions from rest_framework import authentication, exceptions
from common.auth import signature from common.auth import signature
from common.utils import get_object_or_none from common.decorators import merge_delay_run
from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip
from ..models import AccessKey, PrivateToken from ..models import AccessKey, PrivateToken
@@ -16,14 +17,24 @@ def date_more_than(d, seconds):
return d is None or (timezone.now() - d).seconds > seconds return d is None or (timezone.now() - d).seconds > seconds
def after_authenticate_update_date(user, token=None): @merge_delay_run(ttl=60)
if date_more_than(user.date_api_key_last_used, 60): def update_token_last_used(tokens=()):
for token in tokens:
token.date_last_used = timezone.now()
token.save(update_fields=['date_last_used'])
@merge_delay_run(ttl=60)
def update_user_last_used(users=()):
for user in users:
user.date_api_key_last_used = timezone.now() user.date_api_key_last_used = timezone.now()
user.save(update_fields=['date_api_key_last_used']) user.save(update_fields=['date_api_key_last_used'])
if token and hasattr(token, 'date_last_used') and date_more_than(token.date_last_used, 60):
token.date_last_used = timezone.now() def after_authenticate_update_date(user, token=None):
token.save(update_fields=['date_last_used']) update_user_last_used(users=(user,))
if token:
update_token_last_used(tokens=(token,))
class AccessTokenAuthentication(authentication.BaseAuthentication): class AccessTokenAuthentication(authentication.BaseAuthentication):
@@ -122,3 +133,14 @@ class SignatureAuthentication(signature.SignatureAuthentication):
return user, secret return user, secret
except (AccessKey.DoesNotExist, exceptions.ValidationError): except (AccessKey.DoesNotExist, exceptions.ValidationError):
return None, None return None, None
def is_ip_allow(self, key_id, request):
try:
ak = AccessKey.objects.get(id=key_id)
ip_group = ak.ip_group
ip = get_request_ip_or_data(request)
if not contains_ip(ip, ip_group):
return False
return True
except (AccessKey.DoesNotExist, exceptions.ValidationError):
return False

View File

@@ -105,7 +105,7 @@ class OAuth2Backend(JMSModelBackend):
'Accept': 'application/json' 'Accept': 'application/json'
} }
if token_method == 'post': if token_method == 'post':
access_token_response = requests_func(access_token_url, headers=headers, json=query_dict) access_token_response = requests_func(access_token_url, headers=headers, data=query_dict)
else: else:
access_token_response = requests_func(access_token_url, headers=headers) access_token_response = requests_func(access_token_url, headers=headers)
try: try:

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2023-10-31 05:37
import authentication.models.access_key
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0023_auto_20231010_1101'),
]
operations = [
migrations.AddField(
model_name='accesskey',
name='ip_group',
field=models.JSONField(default=authentication.models.access_key.default_ip_group, verbose_name='IP group'),
),
]

View File

@@ -12,9 +12,14 @@ def default_secret():
return random_string(36) return random_string(36)
def default_ip_group():
return ["*"]
class AccessKey(models.Model): class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36) secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)
ip_group = models.JSONField(default=default_ip_group, verbose_name=_('IP group'))
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User', user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys') on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
is_active = models.BooleanField(default=True, verbose_name=_('Active')) is_active = models.BooleanField(default=True, verbose_name=_('Active'))

View File

@@ -97,10 +97,9 @@ class ConnectionToken(JMSOrgBaseModel):
@lazyproperty @lazyproperty
def permed_account(self): def permed_account(self):
from perms.utils import PermAccountUtil from perms.utils import PermAssetDetailUtil
permed_account = PermAccountUtil().validate_permission( permed_account = PermAssetDetailUtil(self.user, self.asset) \
self.user, self.asset, self.account .validate_permission(self.account, self.protocol)
)
return permed_account return permed_account
@lazyproperty @lazyproperty
@@ -115,6 +114,7 @@ class ConnectionToken(JMSOrgBaseModel):
if not self.is_active: if not self.is_active:
error = _('Connection token inactive') error = _('Connection token inactive')
raise PermissionDenied(error) raise PermissionDenied(error)
if self.is_expired: if self.is_expired:
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
raise PermissionDenied(error) raise PermissionDenied(error)

View File

@@ -55,4 +55,4 @@ class IsValidUserOrConnectionToken(IsValidUser):
return False return False
with tmp_to_root_org(): with tmp_to_root_org():
token = get_object_or_none(ConnectionToken, id=token_id) token = get_object_or_none(ConnectionToken, id=token_id)
return token and token.is_valid return token and token.is_valid()

View File

@@ -58,6 +58,8 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_su_from(account): def get_su_from(account):
if not hasattr(account, 'asset'):
return {}
su_enabled = account.asset.platform.su_enabled su_enabled = account.asset.platform.su_enabled
su_from = account.su_from su_from = account.su_from
if not su_from or not su_enabled: if not su_from or not su_enabled:

View File

@@ -4,6 +4,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
from common.utils import get_object_or_none, random_string from common.utils import get_object_or_none, random_string
from users.models import User from users.models import User
from users.serializers import UserProfileSerializer from users.serializers import UserProfileSerializer
@@ -17,9 +18,14 @@ __all__ = [
class AccessKeySerializer(serializers.ModelSerializer): class AccessKeySerializer(serializers.ModelSerializer):
ip_group = serializers.ListField(
default=['*'], label=_('Access IP'), help_text=ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
)
class Meta: class Meta:
model = AccessKey model = AccessKey
fields = ['id', 'is_active', 'date_created', 'date_last_used'] fields = ['id', 'is_active', 'date_created', 'date_last_used'] + ['ip_group']
read_only_fields = ['id', 'date_created', 'date_last_used'] read_only_fields = ['id', 'date_created', 'date_last_used']

View File

@@ -258,7 +258,13 @@
.mobile-logo { .mobile-logo {
display: block; display: block;
padding: 20px 30px 0 30px; padding: 0 30px;
text-align: left;
}
.right-image {
height: revert;
width: revert;
} }
} }
</style> </style>

View File

@@ -17,6 +17,7 @@ Reusing failure exceptions serves several purposes:
""" """
FAILED = exceptions.AuthenticationFailed('Invalid signature.') FAILED = exceptions.AuthenticationFailed('Invalid signature.')
IP_NOT_ALLOW = exceptions.AuthenticationFailed('Ip is not in access ip list.')
class SignatureAuthentication(authentication.BaseAuthentication): class SignatureAuthentication(authentication.BaseAuthentication):
@@ -43,6 +44,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
"""Returns a tuple (User, secret) or (None, None).""" """Returns a tuple (User, secret) or (None, None)."""
raise NotImplementedError() raise NotImplementedError()
def is_ip_allow(self, key_id, request):
raise NotImplementedError()
def authenticate_header(self, request): def authenticate_header(self, request):
""" """
DRF sends this for unauthenticated responses if we're the primary DRF sends this for unauthenticated responses if we're the primary
@@ -50,7 +54,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
""" """
h = " ".join(self.required_headers) h = " ".join(self.required_headers)
return 'Signature realm="%s",headers="%s"' % ( return 'Signature realm="%s",headers="%s"' % (
self.www_authenticate_realm, h) self.www_authenticate_realm, h)
def authenticate(self, request): def authenticate(self, request):
""" """
@@ -78,15 +82,19 @@ class SignatureAuthentication(authentication.BaseAuthentication):
if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0: if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0:
raise FAILED raise FAILED
key_id = fields["keyid"]
# Fetch the secret associated with the keyid # Fetch the secret associated with the keyid
user, secret = self.fetch_user_data( user, secret = self.fetch_user_data(
fields["keyid"], key_id,
algorithm=fields["algorithm"] algorithm=fields["algorithm"]
) )
if not (user and secret): if not (user and secret):
raise FAILED raise FAILED
if not self.is_ip_allow(key_id, request):
raise IP_NOT_ALLOW
# Gather all request headers and translate them as stated in the Django docs: # Gather all request headers and translate them as stated in the Django docs:
# https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META # https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META
headers = {} headers = {}

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