mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-25 05:22:36 +00:00
Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab4562820 | ||
|
|
104dd9721b | ||
|
|
cdcfdeefc5 | ||
|
|
613a7d63b5 | ||
|
|
c6a3a141bb | ||
|
|
93e5a0ba5c | ||
|
|
129c0e1bf4 | ||
|
|
62c57d2fdf | ||
|
|
4711813af8 | ||
|
|
384873b4cb | ||
|
|
33860bb955 | ||
|
|
9e410bb389 | ||
|
|
db2ab1513e | ||
|
|
18e525c943 | ||
|
|
9337463471 | ||
|
|
8fdd89e67c | ||
|
|
c7882a615f | ||
|
|
e6d50cc8b4 | ||
|
|
3bd7410ab8 | ||
|
|
c610ec797f | ||
|
|
188a2846ed | ||
|
|
df99067ee3 | ||
|
|
ca17faaf01 | ||
|
|
a487d30001 | ||
|
|
fae5d07df6 | ||
|
|
df31f47c68 | ||
|
|
d1acab3aa9 | ||
|
|
15363a7f72 | ||
|
|
d573ade525 | ||
|
|
7ac00d5fdf | ||
|
|
2f6c9f8260 | ||
|
|
41732d7a7b | ||
|
|
28d19fd91f | ||
|
|
65269db849 | ||
|
|
df2858470a | ||
|
|
1c8ad40565 | ||
|
|
78de2a2403 | ||
|
|
218f917f69 | ||
|
|
bb25bf7621 | ||
|
|
f6cc7046a2 | ||
|
|
1bc6e50b06 | ||
|
|
1d3135d2d7 | ||
|
|
308d87d021 | ||
|
|
db04f6ca18 | ||
|
|
a7cd0bc0fe | ||
|
|
24708a6c5e | ||
|
|
55a10a8d1d | ||
|
|
32b6a1f1a4 | ||
|
|
c1c70849e9 | ||
|
|
7a6ed91f62 | ||
|
|
497a52a509 | ||
|
|
57e12256e7 | ||
|
|
b8ec60dea1 | ||
|
|
c9afd94714 | ||
|
|
a0c61ab8cb | ||
|
|
567b62516a | ||
|
|
404fadd899 | ||
|
|
ee1ec6aeee | ||
|
|
783bddf2c7 | ||
|
|
5ae49295e9 | ||
|
|
8d6d188ac7 | ||
|
|
912ff3df24 | ||
|
|
995d8cadb9 | ||
|
|
6e5cea49ae | ||
|
|
a33a452434 | ||
|
|
fe2f54fcf6 | ||
|
|
1e3154d9b6 | ||
|
|
a1c09591d3 | ||
|
|
d4e0a51a08 | ||
|
|
bba4c15d6d | ||
|
|
3e33c74b64 | ||
|
|
556d29360e | ||
|
|
9329a1563c | ||
|
|
8bf11c9ade | ||
|
|
bbb802d894 | ||
|
|
8e7226d9dc | ||
|
|
2bd889e505 | ||
|
|
3dcfd0035a | ||
|
|
edfda5825c | ||
|
|
3a196f0814 | ||
|
|
a4a671afd4 | ||
|
|
c337bbff8f | ||
|
|
863140e185 | ||
|
|
ad0d264c2a | ||
|
|
7f85e503d5 | ||
|
|
61ff3db0f1 | ||
|
|
fa08517bea | ||
|
|
f86d045c01 | ||
|
|
1a7fd58abf | ||
|
|
d808256e6a | ||
|
|
305a1b10ed | ||
|
|
8c277e8875 | ||
|
|
ca965aca9e | ||
|
|
061b60ef59 | ||
|
|
c008115888 | ||
|
|
8d1fb84aaf | ||
|
|
43d61b5348 | ||
|
|
c26a786287 | ||
|
|
cb2bd0cf2c | ||
|
|
3048e6311b | ||
|
|
5e16b6387a | ||
|
|
93e1adf376 | ||
|
|
556bd3682e | ||
|
|
6bbbe312a2 | ||
|
|
1ac64db0ba | ||
|
|
fa54a98d6c | ||
|
|
31de9375e7 | ||
|
|
697270e3e6 | ||
|
|
56c324b04e | ||
|
|
984b94c874 | ||
|
|
50df7f1304 | ||
|
|
7bd7be78a4 | ||
|
|
8e5833aef0 | ||
|
|
f20b465ddf | ||
|
|
409d254a2e | ||
|
|
e6d30fa77d | ||
|
|
b25404cac1 | ||
|
|
ef4cc5f646 | ||
|
|
f0dc519423 | ||
|
|
2cb6da3129 | ||
|
|
1819083a25 | ||
|
|
bdeec0d3cb | ||
|
|
8fc5c4cf9e | ||
|
|
89051b2c67 | ||
|
|
9123839b48 | ||
|
|
258c8a30d1 | ||
|
|
af75b5269c | ||
|
|
0a66693a41 | ||
|
|
7151201d58 | ||
|
|
51820f23bf | ||
|
|
8772cd8c71 | ||
|
|
60cb1f8136 | ||
|
|
5f1b7ff8f9 | ||
|
|
37b150bc04 | ||
|
|
1432fe1609 | ||
|
|
8ae98887ee | ||
|
|
24a1738e73 | ||
|
|
188c04c9a6 | ||
|
|
bb4da12366 | ||
|
|
382112ee33 | ||
|
|
3e69e6840b | ||
|
|
a82ed3e924 | ||
|
|
b347acd5ec | ||
|
|
ccd6b01020 | ||
|
|
831b67eae4 | ||
|
|
3ab634d88e | ||
|
|
867ad94a30 | ||
|
|
7d0a19635a | ||
|
|
4642804077 | ||
|
|
d405bae205 | ||
|
|
68841d1f15 | ||
|
|
4cad5affec | ||
|
|
2f8a07e665 | ||
|
|
78133b0c60 | ||
|
|
88d9078c43 | ||
|
|
5559f112db | ||
|
|
9a4b32cb3c | ||
|
|
ddf4b61c9f | ||
|
|
0eaaa7b4f6 | ||
|
|
09160fed5d | ||
|
|
18af5e8c4a | ||
|
|
1ed388459b | ||
|
|
2e944c6898 | ||
|
|
8409523fee | ||
|
|
16634907b4 | ||
|
|
cfa5de13ab | ||
|
|
28c8ec1fab | ||
|
|
a14ebc5f0f | ||
|
|
6af20d298d | ||
|
|
795d6e01dc | ||
|
|
acf8b5798b | ||
|
|
abcd12f645 | ||
|
|
30fe5214c7 | ||
|
|
708a87c903 | ||
|
|
6a30e0739d | ||
|
|
3951b8b080 | ||
|
|
c295f1451a | ||
|
|
c4a94876cc | ||
|
|
dcab934d9f | ||
|
|
4ecb0b760f | ||
|
|
b27b02eb9d | ||
|
|
70cf847cd9 | ||
|
|
2099baaaff | ||
|
|
b22aed0cc3 | ||
|
|
3e7f83d44e | ||
|
|
40f8b99242 | ||
|
|
9ff345747b | ||
|
|
9319c4748c | ||
|
|
e8b4ee5c40 | ||
|
|
429e838973 | ||
|
|
ee1aff243c | ||
|
|
ea7133dea0 | ||
|
|
e7229963bf | ||
|
|
0f7b41d177 | ||
|
|
c4146744e5 | ||
|
|
dc32224294 | ||
|
|
d07a230ba6 |
@@ -7,4 +7,5 @@ django.db
|
||||
celerybeat.pid
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
apps/xpack/.git
|
||||
|
||||
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,3 +1,4 @@
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
3
.github/release-config.yml
vendored
3
.github/release-config.yml
vendored
@@ -41,4 +41,5 @@ version-resolver:
|
||||
default: patch
|
||||
template: |
|
||||
## 版本变化 What’s Changed
|
||||
$CHANGES
|
||||
$CHANGES
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ release/*
|
||||
releashe
|
||||
/apps/script.py
|
||||
data/*
|
||||
|
||||
|
||||
@@ -126,3 +126,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
|
||||
@@ -23,3 +23,4 @@ When reporting issues, always include:
|
||||
|
||||
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
|
||||
replace those parts with "REDACTED" or other strings like "****".
|
||||
|
||||
|
||||
113
Dockerfile
113
Dockerfile
@@ -1,54 +1,66 @@
|
||||
FROM python:3.8-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.8-slim
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev \
|
||||
sshpass"
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev \
|
||||
openssh-client \
|
||||
sshpass"
|
||||
|
||||
ARG TOOLS=" \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
locales \
|
||||
procps \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
procps \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& apt update && sleep 1 && apt update \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& apt -y install ${TOOLS} \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@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} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG ORACLE_LIB_MAJOR=19
|
||||
ARG ORACLE_LIB_MINOR=10
|
||||
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
|
||||
@@ -65,25 +77,22 @@ RUN mkdir -p /opt/oracle/ \
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install --upgrade setuptools wheel \
|
||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install -r requirements/requirements.txt
|
||||
|
||||
ADD . .
|
||||
RUN cd utils \
|
||||
&& bash -ixeu build.sh \
|
||||
&& mv ../release/jumpserver /opt/jumpserver \
|
||||
&& rm -rf /tmp/build \
|
||||
&& echo > /opt/jumpserver/config.yml
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& rm -rf /tmp/build
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
|
||||
95
Dockerfile.loong64
Normal file
95
Dockerfile.loong64
Normal file
@@ -0,0 +1,95 @@
|
||||
FROM python:3.8-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.8-slim
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
default-libmysqlclient-dev \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev \
|
||||
openssh-client \
|
||||
sshpass"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-mysql-client \
|
||||
iputils-ping \
|
||||
locales \
|
||||
netcat \
|
||||
redis-server \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& 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} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install --upgrade setuptools wheel \
|
||||
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \
|
||||
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \
|
||||
&& pip install $(grep 'PyNaCl' requirements/requirements.txt) \
|
||||
&& GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \
|
||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& rm -rf /tmp/build
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8070
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
3
LICENSE
3
LICENSE
@@ -671,4 +671,5 @@ into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -16,15 +16,13 @@
|
||||
|
||||
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
改变世界,从一点点开始 ...
|
||||
|
||||
> 如需进一步了解 JumpServer 开源项目,推荐阅读 [JumpServer 的初心和使命](https://mp.weixin.qq.com/s/S6q_2rP_9MwaVwyqLQnXzA)
|
||||
|
||||
### 特色优势
|
||||
|
||||
@@ -95,11 +93,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
|
||||
- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
|
||||
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
|
||||
- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
|
||||
- [携程:JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [大智慧:JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [小红书:JumpServer 堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [中手游:JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
|
||||
|
||||
@@ -92,4 +92,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
|
||||
https://www.gnu.org/licenses/gpl-3.0.htmll
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
@@ -18,3 +18,4 @@ All security bugs should be reported to the contact as below:
|
||||
- ibuler@fit2cloud.com
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
|
||||
56
Vagrantfile
vendored
56
Vagrantfile
vendored
@@ -1,56 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
# The most common configuration options are documented and commented below.
|
||||
# For a complete reference, please see the online documentation at
|
||||
# https://docs.vagrantup.com.
|
||||
|
||||
# Every Vagrant development environment requires a box. You can search for
|
||||
# boxes at https://vagrantcloud.com/search.
|
||||
config.vm.box_check_update = false
|
||||
config.vm.box = "centos/7"
|
||||
config.vm.hostname = "jumpserver"
|
||||
config.vm.network "private_network", ip: "172.17.8.101"
|
||||
config.vm.provider "virtualbox" do |vb|
|
||||
vb.memory = "4096"
|
||||
vb.cpus = 2
|
||||
vb.name = "jumpserver"
|
||||
end
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant", type: "rsync",
|
||||
rsync__verbose: true,
|
||||
rsync__exclude: ['.git*', 'node_modules*','*.log','*.box','Vagrantfile']
|
||||
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
## 设置yum的阿里云源
|
||||
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
|
||||
sudo sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo
|
||||
sudo curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
|
||||
sudo yum makecache
|
||||
|
||||
## 安装依赖包
|
||||
sudo yum install -y python36 python36-devel python36-pip \
|
||||
libtiff-devel libjpeg-devel libzip-devel freetype-devel \
|
||||
lcms2-devel libwebp-devel tcl-devel tk-devel sshpass \
|
||||
openldap-devel mariadb-devel mysql-devel libffi-devel \
|
||||
openssh-clients telnet openldap-clients gcc
|
||||
|
||||
## 配置pip阿里云源
|
||||
mkdir /home/vagrant/.pip
|
||||
cat << EOF | sudo tee -a /home/vagrant/.pip/pip.conf
|
||||
[global]
|
||||
timeout = 6000
|
||||
index-url = https://mirrors.aliyun.com/pypi/simple/
|
||||
|
||||
[install]
|
||||
use-mirrors = true
|
||||
mirrors = https://mirrors.aliyun.com/pypi/simple/
|
||||
trusted-host=mirrors.aliyun.com
|
||||
EOF
|
||||
|
||||
python3.6 -m venv /home/vagrant/venv
|
||||
source /home/vagrant/venv/bin/activate
|
||||
echo 'source /home/vagrant/venv/bin/activate' >> /home/vagrant/.bash_profile
|
||||
SHELL
|
||||
end
|
||||
@@ -44,58 +44,29 @@ class LoginACL(BaseACL):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
||||
@property
|
||||
def action_allow(self):
|
||||
return self.action == self.ActionChoices.allow
|
||||
def is_action(self, action):
|
||||
return self.action == action
|
||||
|
||||
@classmethod
|
||||
def filter_acl(cls, user):
|
||||
return user.login_acls.all().valid().distinct()
|
||||
|
||||
@staticmethod
|
||||
def allow_user_confirm_if_need(user, ip):
|
||||
acl = LoginACL.filter_acl(user).filter(
|
||||
action=LoginACL.ActionChoices.confirm
|
||||
).first()
|
||||
acl = acl if acl and acl.reviewers.exists() else None
|
||||
if not acl:
|
||||
return False, acl
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
return is_contain_ip and is_contain_time_period, acl
|
||||
def match(user, ip):
|
||||
acls = LoginACL.filter_acl(user)
|
||||
if not acls:
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def allow_user_to_login(user, ip):
|
||||
acl = LoginACL.filter_acl(user).exclude(
|
||||
action=LoginACL.ActionChoices.confirm
|
||||
).first()
|
||||
if not acl:
|
||||
return True, ''
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
|
||||
reject_type = ''
|
||||
if is_contain_ip and is_contain_time_period:
|
||||
# 满足条件
|
||||
allow = acl.action_allow
|
||||
if not allow:
|
||||
reject_type = 'ip' if is_contain_ip else 'time'
|
||||
else:
|
||||
# 不满足条件
|
||||
# 如果acl本身允许,那就拒绝;如果本身拒绝,那就允许
|
||||
allow = not acl.action_allow
|
||||
if not allow:
|
||||
reject_type = 'ip' if not is_contain_ip else 'time'
|
||||
|
||||
return allow, reject_type
|
||||
for acl in acls:
|
||||
if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists():
|
||||
continue
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
if is_contain_ip and is_contain_time_period:
|
||||
# 满足条件,则返回
|
||||
return acl
|
||||
|
||||
def create_confirm_ticket(self, request):
|
||||
from tickets import const
|
||||
|
||||
@@ -4,6 +4,7 @@ from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
from common.tree import TreeNodeSerializer
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from .. import serializers
|
||||
|
||||
@@ -7,3 +7,7 @@ from django.apps import AppConfig
|
||||
class ApplicationsConfig(AppConfig):
|
||||
name = 'applications'
|
||||
verbose_name = _('Applications')
|
||||
|
||||
def ready(self):
|
||||
from . import signal_handlers
|
||||
super().ready()
|
||||
|
||||
@@ -83,9 +83,3 @@ class AppType(models.TextChoices):
|
||||
if AppCategory.is_xpack(category):
|
||||
return True
|
||||
return tp in ['oracle', 'postgresql', 'sqlserver']
|
||||
|
||||
|
||||
class OracleVersion(models.TextChoices):
|
||||
version_11g = '11g', '11g'
|
||||
version_12c = '12c', '12c'
|
||||
version_other = 'other', _('Other')
|
||||
|
||||
@@ -10,9 +10,7 @@ from common.mixins import CommonModelMixin
|
||||
from common.tree import TreeNode
|
||||
from common.utils import is_uuid
|
||||
from assets.models import Asset, SystemUser
|
||||
from ..const import OracleVersion
|
||||
|
||||
from ..utils import KubernetesTree
|
||||
from .. import const
|
||||
|
||||
|
||||
@@ -175,6 +173,7 @@ class ApplicationTreeNodeMixin:
|
||||
return pid
|
||||
|
||||
def as_tree_node(self, pid, k8s_as_tree=False):
|
||||
from ..utils import KubernetesTree
|
||||
if self.type == const.AppType.k8s and k8s_as_tree:
|
||||
node = KubernetesTree(pid).as_tree_node(self)
|
||||
else:
|
||||
@@ -304,15 +303,6 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
target_ip = self.attrs.get('host')
|
||||
return target_ip
|
||||
|
||||
def get_target_protocol_for_oracle(self):
|
||||
""" Oracle 类型需要单独处理,因为要携带版本号 """
|
||||
if not self.is_type(self.APP_TYPE.oracle):
|
||||
return
|
||||
version = self.attrs.get('version', OracleVersion.version_12c)
|
||||
if version == OracleVersion.version_other:
|
||||
return
|
||||
return 'oracle_%s' % version
|
||||
|
||||
|
||||
class ApplicationUser(SystemUser):
|
||||
class Meta:
|
||||
|
||||
@@ -16,7 +16,7 @@ from .. import const
|
||||
|
||||
__all__ = [
|
||||
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
|
||||
'AppAccountSerializer', 'AppAccountSecretSerializer'
|
||||
'AppAccountSerializer', 'AppAccountSecretSerializer', 'AppAccountBackUpSerializer'
|
||||
]
|
||||
|
||||
|
||||
@@ -32,21 +32,23 @@ class AppSerializerMixin(serializers.Serializer):
|
||||
return instance
|
||||
|
||||
def get_attrs_serializer(self):
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
instance = self.app
|
||||
if instance:
|
||||
_type = instance.type
|
||||
_category = instance.category
|
||||
else:
|
||||
_type = self.context['request'].query_params.get('type')
|
||||
_category = self.context['request'].query_params.get('category')
|
||||
if _type:
|
||||
if isinstance(self, AppAccountSecretSerializer):
|
||||
serializer_class = type_secret_serializer_classes_mapping.get(_type)
|
||||
tp = getattr(self, 'tp', None)
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
if not tp:
|
||||
if instance:
|
||||
tp = instance.type
|
||||
category = instance.category
|
||||
else:
|
||||
serializer_class = type_serializer_classes_mapping.get(_type)
|
||||
elif _category:
|
||||
serializer_class = category_serializer_classes_mapping.get(_category)
|
||||
tp = self.context['request'].query_params.get('type')
|
||||
category = self.context['request'].query_params.get('category')
|
||||
if tp:
|
||||
if isinstance(self, AppAccountBackUpSerializer):
|
||||
serializer_class = type_secret_serializer_classes_mapping.get(tp)
|
||||
else:
|
||||
serializer_class = type_serializer_classes_mapping.get(tp)
|
||||
elif category:
|
||||
serializer_class = category_serializer_classes_mapping.get(category)
|
||||
else:
|
||||
serializer_class = default_serializer
|
||||
|
||||
@@ -154,11 +156,6 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
|
||||
|
||||
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
class Meta(AppAccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
@@ -166,3 +163,22 @@ class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
'app_display': {'label': _('Application display')},
|
||||
'systemuser_display': {'label': _('System User')}
|
||||
}
|
||||
|
||||
|
||||
class AppAccountBackUpSerializer(AppAccountSecretSerializer):
|
||||
class Meta(AppAccountSecretSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tp = kwargs.pop('tp', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
return queryset
|
||||
|
||||
def to_representation(self, instance):
|
||||
return super(AppAccountSerializer, self).to_representation(instance)
|
||||
|
||||
@@ -13,3 +13,14 @@ class DBSerializer(serializers.Serializer):
|
||||
database = serializers.CharField(
|
||||
max_length=128, required=True, allow_null=True, label=_('Database')
|
||||
)
|
||||
use_ssl = serializers.BooleanField(default=False, label=_('Use SSL'))
|
||||
ca_cert = serializers.CharField(
|
||||
required=False, allow_null=True, label=_('CA certificate')
|
||||
)
|
||||
client_cert = serializers.CharField(
|
||||
required=False, allow_null=True, label=_('Client certificate file')
|
||||
)
|
||||
cert_key = serializers.CharField(
|
||||
required=False, allow_null=True, label=_('Certificate key file')
|
||||
)
|
||||
allow_invalid_cert = serializers.BooleanField(default=False, label=_('Allow invalid cert'))
|
||||
|
||||
@@ -2,15 +2,9 @@ from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..application_category import DBSerializer
|
||||
from applications.const import OracleVersion
|
||||
|
||||
__all__ = ['OracleSerializer']
|
||||
|
||||
|
||||
class OracleSerializer(DBSerializer):
|
||||
version = serializers.ChoiceField(
|
||||
choices=OracleVersion.choices, default=OracleVersion.version_12c,
|
||||
allow_null=True, label=_('Version'),
|
||||
help_text=_('Magnus currently supports only 11g and 12c connections')
|
||||
)
|
||||
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)
|
||||
|
||||
2
apps/applications/signal_handlers.py
Normal file
2
apps/applications/signal_handlers.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
@@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from common.mixins.api import SuggestionMixin, RenderToJsonMixin
|
||||
from users.models import User, UserGroup
|
||||
from users.serializers import UserSerializer, UserGroupSerializer
|
||||
from users.filters import UserFilter
|
||||
@@ -88,7 +88,7 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
return asset.platform
|
||||
|
||||
|
||||
class AssetPlatformViewSet(ModelViewSet):
|
||||
class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_fields = ['name', 'base']
|
||||
|
||||
@@ -24,7 +24,7 @@ class SerializeToTreeNodeMixin:
|
||||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'open': node.is_org_root(),
|
||||
'open': True,
|
||||
'meta': {
|
||||
'data': {
|
||||
"id": node.id,
|
||||
|
||||
@@ -101,6 +101,8 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
|
||||
class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
serializer_class = serializers.NodeSerializer
|
||||
search_fields = ('value',)
|
||||
|
||||
instance = None
|
||||
is_initial = False
|
||||
|
||||
@@ -179,8 +181,15 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
"""
|
||||
model = Node
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not self.request.GET.get('search'):
|
||||
return queryset
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.model.get_ancestor_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
nodes = self.get_queryset().order_by('value')
|
||||
nodes = self.filter_queryset(self.get_queryset()).order_by('value')
|
||||
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
|
||||
assets = self.get_assets()
|
||||
data = [*nodes, *assets]
|
||||
|
||||
@@ -208,7 +208,7 @@ class SystemUserTaskApi(generics.CreateAPIView):
|
||||
|
||||
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
rbac_perms = {
|
||||
'list': 'assets.view_commandfilterule'
|
||||
'list': 'assets.view_commandfilterule',
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
@@ -223,12 +223,14 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
|
||||
if not system_user:
|
||||
system_user_id = self.request.query_params.get('system_user_id')
|
||||
asset_id = self.request.query_params.get('asset_id')
|
||||
node_id = self.request.query_params.get('node_id')
|
||||
application_id = self.request.query_params.get('application_id')
|
||||
rules = CommandFilterRule.get_queryset(
|
||||
user_id=user_id,
|
||||
user_group_id=user_group_id,
|
||||
system_user_id=system_user_id,
|
||||
asset_id=asset_id,
|
||||
node_id=node_id,
|
||||
application_id=application_id
|
||||
)
|
||||
return rules
|
||||
|
||||
18
apps/assets/migrations/0092_commandfilter_nodes.py
Normal file
18
apps/assets/migrations/0092_commandfilter_nodes.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.15 on 2022-10-09 09:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0091_auto_20220629_1826'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='commandfilter',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='assets.Node', verbose_name='Nodes'),
|
||||
),
|
||||
]
|
||||
@@ -116,9 +116,9 @@ class NodesRelationMixin:
|
||||
nodes = []
|
||||
for node in self.get_nodes():
|
||||
_nodes = node.get_ancestors(with_self=True)
|
||||
nodes.append(_nodes)
|
||||
nodes.extend(list(_nodes))
|
||||
if flat:
|
||||
nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
|
||||
nodes = list(set([node.id for node in nodes]))
|
||||
return nodes
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from users.models import User, UserGroup
|
||||
from applications.models import Application
|
||||
from ..models import SystemUser, Asset
|
||||
from ..models import SystemUser, Asset, Node
|
||||
|
||||
from common.utils import lazyproperty, get_logger, get_object_or_none
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
@@ -33,6 +33,10 @@ class CommandFilter(OrgModelMixin):
|
||||
'users.UserGroup', related_name='cmd_filters', blank=True,
|
||||
verbose_name=_("User group"),
|
||||
)
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', related_name='cmd_filters', blank=True,
|
||||
verbose_name=_("Nodes")
|
||||
)
|
||||
assets = models.ManyToManyField(
|
||||
'assets.Asset', related_name='cmd_filters', blank=True,
|
||||
verbose_name=_("Asset")
|
||||
@@ -189,7 +193,8 @@ class CommandFilterRule(OrgModelMixin):
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
|
||||
asset_id=None, application_id=None, org_id=None):
|
||||
asset_id=None, node_id=None, application_id=None, org_id=None):
|
||||
# user & user_group
|
||||
user_groups = []
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if user:
|
||||
@@ -198,8 +203,18 @@ class CommandFilterRule(OrgModelMixin):
|
||||
if user_group:
|
||||
org_id = user_group.org_id
|
||||
user_groups.append(user_group)
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
|
||||
# asset & node
|
||||
nodes = []
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
if asset:
|
||||
nodes.extend(asset.get_all_nodes())
|
||||
node = get_object_or_none(Node, pk=node_id)
|
||||
if node:
|
||||
org_id = node.org_id
|
||||
nodes.extend(list(node.get_ancestors(with_self=True)))
|
||||
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
application = get_object_or_none(Application, pk=application_id)
|
||||
q = Q()
|
||||
if user:
|
||||
@@ -212,6 +227,8 @@ class CommandFilterRule(OrgModelMixin):
|
||||
if asset:
|
||||
org_id = asset.org_id
|
||||
q |= Q(assets=asset)
|
||||
if nodes:
|
||||
q |= Q(nodes__in=set(nodes))
|
||||
if application:
|
||||
org_id = application.org_id
|
||||
q |= Q(applications=application)
|
||||
|
||||
@@ -25,7 +25,6 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -98,6 +97,14 @@ class FamilyMixin:
|
||||
q |= Q(key=self.key)
|
||||
return Node.objects.filter(q)
|
||||
|
||||
@classmethod
|
||||
def get_ancestor_queryset(cls, queryset, with_self=True):
|
||||
parent_keys = set()
|
||||
for i in queryset:
|
||||
parent_keys.update(set(i.get_ancestor_keys(with_self=with_self)))
|
||||
queryset = queryset.model.objects.filter(key__in=list(parent_keys)).distinct()
|
||||
return queryset
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self.get_children(with_self=False)
|
||||
@@ -396,7 +403,7 @@ class NodeAllAssetsMappingMixin:
|
||||
mapping[ancestor_key].update(asset_ids)
|
||||
|
||||
t3 = time.time()
|
||||
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
|
||||
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2 - t1, t3 - t2))
|
||||
return mapping
|
||||
|
||||
|
||||
|
||||
@@ -76,10 +76,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
|
||||
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
@@ -88,6 +84,22 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
}
|
||||
|
||||
|
||||
class AccountBackUpSerializer(AccountSecretSerializer):
|
||||
class Meta(AccountSecretSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'hostname', 'ip', 'username', 'password',
|
||||
'private_key', 'public_key', 'date_created',
|
||||
'date_updated', 'version'
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
return queryset
|
||||
|
||||
def to_representation(self, instance):
|
||||
return super(AccountSerializer, self).to_representation(instance)
|
||||
|
||||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
|
||||
@@ -189,6 +189,9 @@ class PlatformSerializer(serializers.ModelSerializer):
|
||||
'id', 'name', 'base', 'charset',
|
||||
'internal', 'meta', 'comment'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'internal': {'read_only': True},
|
||||
}
|
||||
|
||||
|
||||
class AssetSimpleSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -21,7 +21,7 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_fk = ['rules']
|
||||
fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications']
|
||||
fields_m2m = ['users', 'user_groups', 'system_users', 'nodes', 'assets', 'applications']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
extra_kwargs = {
|
||||
'rules': {'read_only': True},
|
||||
|
||||
@@ -4,15 +4,16 @@ from openpyxl import Workbook
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.models import AuthBook
|
||||
from assets.serializers import AccountSecretSerializer
|
||||
from assets.models import AuthBook, SystemUser, Asset
|
||||
from assets.serializers import AccountBackUpSerializer
|
||||
from assets.notifications import AccountBackupExecutionTaskMsg
|
||||
from applications.models import Account
|
||||
from applications.models import Account, Application
|
||||
from applications.const import AppType
|
||||
from applications.serializers import AppAccountSecretSerializer
|
||||
from applications.serializers import AppAccountBackUpSerializer
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
@@ -38,7 +39,7 @@ class BaseAccountHandler:
|
||||
@classmethod
|
||||
def get_header_fields(cls, serializer: serializers.Serializer):
|
||||
try:
|
||||
backup_fields = getattr(serializer, 'Meta').fields_backup
|
||||
backup_fields = getattr(serializer, 'Meta').fields
|
||||
except AttributeError:
|
||||
backup_fields = serializer.fields.keys()
|
||||
header_fields = {}
|
||||
@@ -51,17 +52,41 @@ class BaseAccountHandler:
|
||||
header_fields[field] = str(v.label)
|
||||
return header_fields
|
||||
|
||||
@staticmethod
|
||||
def load_auth(tp, value, system_user):
|
||||
if value:
|
||||
return value
|
||||
if system_user:
|
||||
return getattr(system_user, tp, '')
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def create_row(cls, account, serializer_cls, header_fields=None):
|
||||
serializer = serializer_cls(account)
|
||||
if not header_fields:
|
||||
header_fields = cls.get_header_fields(serializer)
|
||||
data = cls.unpack_data(serializer.data)
|
||||
def replace_auth(cls, account, system_user_dict):
|
||||
system_user = system_user_dict.get(account.systemuser_id)
|
||||
account.username = cls.load_auth('username', account.username, system_user)
|
||||
account.password = cls.load_auth('password', account.password, system_user)
|
||||
account.private_key = cls.load_auth('private_key', account.private_key, system_user)
|
||||
account.public_key = cls.load_auth('public_key', account.public_key, system_user)
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def create_row(cls, data, header_fields):
|
||||
data = cls.unpack_data(data)
|
||||
row_dict = {}
|
||||
for field, header_name in header_fields.items():
|
||||
row_dict[header_name] = str(data[field])
|
||||
row_dict[header_name] = str(data.get(field, field))
|
||||
return row_dict
|
||||
|
||||
@classmethod
|
||||
def add_rows(cls, data, header_fields, sheet):
|
||||
data_map = defaultdict(list)
|
||||
for i in data:
|
||||
row = cls.create_row(i, header_fields)
|
||||
if sheet not in data_map:
|
||||
data_map[sheet].append(list(row.keys()))
|
||||
data_map[sheet].append(list(row.values()))
|
||||
return data_map
|
||||
|
||||
|
||||
class AssetAccountHandler(BaseAccountHandler):
|
||||
@staticmethod
|
||||
@@ -72,22 +97,27 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls):
|
||||
data_map = defaultdict(list)
|
||||
def replace_account_info(cls, account, asset_dict, system_user_dict):
|
||||
asset = asset_dict.get(account.asset_id)
|
||||
account.ip = asset.ip if asset else ''
|
||||
account.hostname = asset.hostname if asset else ''
|
||||
account = cls.replace_auth(account, system_user_dict)
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls, system_user_dict):
|
||||
sheet_name = AuthBook._meta.verbose_name
|
||||
assets = Asset.objects.only('id', 'hostname', 'ip')
|
||||
asset_dict = {asset.id: asset for asset in assets}
|
||||
accounts = AuthBook.objects.all()
|
||||
if not accounts.exists():
|
||||
return
|
||||
|
||||
accounts = AuthBook.get_queryset().select_related('systemuser')
|
||||
if not accounts.first():
|
||||
return data_map
|
||||
|
||||
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
|
||||
header_fields = cls.get_header_fields(AccountBackUpSerializer(accounts.first()))
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
row = cls.create_row(account, AccountSecretSerializer, header_fields)
|
||||
if sheet_name not in data_map:
|
||||
data_map[sheet_name].append(list(row.keys()))
|
||||
data_map[sheet_name].append(list(row.values()))
|
||||
|
||||
cls.replace_account_info(account, asset_dict, system_user_dict)
|
||||
data = AccountBackUpSerializer(accounts, many=True).data
|
||||
data_map = cls.add_rows(data, header_fields, sheet_name)
|
||||
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
|
||||
return data_map
|
||||
|
||||
@@ -101,18 +131,36 @@ class AppAccountHandler(BaseAccountHandler):
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls):
|
||||
data_map = defaultdict(list)
|
||||
accounts = Account.get_queryset().select_related('systemuser')
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
app_type = account.type
|
||||
def replace_account_info(cls, account, app_dict, system_user_dict):
|
||||
app = app_dict.get(account.app_id)
|
||||
account.type = app.type if app else ''
|
||||
account.app_display = app.name if app else ''
|
||||
account.category = app.category if app else ''
|
||||
account = cls.replace_auth(account, system_user_dict)
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls, system_user_dict):
|
||||
apps = Application.objects.only('id', 'type', 'name', 'category')
|
||||
app_dict = {app.id: app for app in apps}
|
||||
qs = Account.objects.all().annotate(app_type=F('app__type'))
|
||||
if not qs.exists():
|
||||
return
|
||||
|
||||
account_type_map = defaultdict(list)
|
||||
for i in qs:
|
||||
account_type_map[i.app_type].append(i)
|
||||
data_map = {}
|
||||
for app_type, accounts in account_type_map.items():
|
||||
sheet_name = AppType.get_label(app_type)
|
||||
row = cls.create_row(account, AppAccountSecretSerializer)
|
||||
if sheet_name not in data_map:
|
||||
data_map[sheet_name].append(list(row.keys()))
|
||||
data_map[sheet_name].append(list(row.values()))
|
||||
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
|
||||
header_fields = cls.get_header_fields(AppAccountBackUpSerializer(tp=app_type))
|
||||
if not accounts:
|
||||
continue
|
||||
for account in accounts:
|
||||
cls.replace_account_info(account, app_dict, system_user_dict)
|
||||
data = AppAccountBackUpSerializer(accounts, many=True, tp=app_type).data
|
||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(qs.count()))
|
||||
return data_map
|
||||
|
||||
|
||||
@@ -137,12 +185,16 @@ class AccountBackupHandler:
|
||||
# Print task start date
|
||||
time_start = time.time()
|
||||
files = []
|
||||
system_user_qs = SystemUser.objects.only(
|
||||
'id', 'username', 'password', 'private_key', 'public_key'
|
||||
)
|
||||
system_user_dict = {i.id: i for i in system_user_qs}
|
||||
for account_type in self.execution.types:
|
||||
handler = handler_map.get(account_type)
|
||||
if not handler:
|
||||
continue
|
||||
|
||||
data_map = handler.create_data_map()
|
||||
data_map = handler.create_data_map(system_user_dict)
|
||||
if not data_map:
|
||||
continue
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from common.api import CommonGenericViewSet
|
||||
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
from . import filters
|
||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
|
||||
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
|
||||
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
|
||||
@@ -126,9 +127,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
|
||||
serializer_class = CommandExecutionHostsRelationSerializer
|
||||
m2m_field = CommandExecution.hosts.field
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'commandexecution'
|
||||
]
|
||||
filterset_class = filters.CommandExecutionFilter
|
||||
search_fields = ('asset__hostname', )
|
||||
http_method_names = ['options', 'get']
|
||||
rbac_perms = {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django_filters.rest_framework import CharFilter
|
||||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
|
||||
__all__ = ['CurrentOrgMembersFilter']
|
||||
__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter']
|
||||
|
||||
|
||||
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
||||
@@ -30,3 +34,22 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
||||
else:
|
||||
queryset = queryset.filter(user__in=self._get_user_list())
|
||||
return queryset
|
||||
|
||||
|
||||
class CommandExecutionFilter(BaseFilterSet):
|
||||
hostname_ip = CharFilter(method='filter_hostname_ip')
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution.hosts.through
|
||||
fields = (
|
||||
'id', 'asset', 'commandexecution', 'hostname_ip'
|
||||
)
|
||||
|
||||
def filter_hostname_ip(self, queryset, name, value):
|
||||
queryset = queryset.annotate(
|
||||
hostname_ip=Concat(
|
||||
F('asset__hostname'), Value('('),
|
||||
F('asset__ip'), Value(')')
|
||||
)
|
||||
).filter(hostname_ip__icontains=value)
|
||||
return queryset
|
||||
|
||||
@@ -22,7 +22,6 @@ from ..serializers import (
|
||||
)
|
||||
from ..models import ConnectionToken
|
||||
|
||||
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
|
||||
|
||||
@@ -63,12 +62,15 @@ class ConnectionTokenMixin:
|
||||
|
||||
def get_smart_endpoint(self, protocol, asset=None, application=None):
|
||||
if asset:
|
||||
target_instance = asset
|
||||
target_ip = asset.get_target_ip()
|
||||
elif application:
|
||||
target_instance = application
|
||||
target_ip = application.get_target_ip()
|
||||
else:
|
||||
target_instance = None
|
||||
target_ip = ''
|
||||
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
|
||||
endpoint = EndpointRule.match_endpoint(target_instance, target_ip, protocol, self.request)
|
||||
return endpoint
|
||||
|
||||
@staticmethod
|
||||
@@ -174,9 +176,8 @@ class ConnectionTokenMixin:
|
||||
rdp_options['remoteapplicationname:s'] = name
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
filename = "{}-{}-jumpserver".format(token.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
prefix_name = f'{token.user.username}-{name}'
|
||||
filename = self.get_connect_filename(prefix_name)
|
||||
|
||||
content = ''
|
||||
for k, v in rdp_options.items():
|
||||
@@ -184,6 +185,15 @@ class ConnectionTokenMixin:
|
||||
|
||||
return filename, content
|
||||
|
||||
@staticmethod
|
||||
def get_connect_filename(prefix_name):
|
||||
prefix_name = prefix_name.replace('/', '_')
|
||||
prefix_name = prefix_name.replace('\\', '_')
|
||||
prefix_name = prefix_name.replace('.', '_')
|
||||
filename = f'{prefix_name}-jumpserver'
|
||||
filename = urllib.parse.quote(filename)
|
||||
return filename
|
||||
|
||||
def get_ssh_token(self, token: ConnectionToken):
|
||||
if token.asset:
|
||||
name = token.asset.hostname
|
||||
@@ -191,7 +201,8 @@ class ConnectionTokenMixin:
|
||||
name = token.application.name
|
||||
else:
|
||||
name = '*'
|
||||
filename = f'{token.user.username}-{name}-jumpserver'
|
||||
prefix_name = f'{token.user.username}-{name}'
|
||||
filename = self.get_connect_filename(prefix_name)
|
||||
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='ssh', asset=token.asset, application=token.application
|
||||
@@ -326,4 +337,3 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
'msg': f'Token is renewed, date expired: {date_expired}'
|
||||
}
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
from common.utils import get_logger
|
||||
from .. import errors, mixins
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
|
||||
__all__ = ['TicketStatusApi']
|
||||
logger = get_logger(__name__)
|
||||
@@ -17,7 +19,15 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.check_user_login_confirm()
|
||||
self.request.session['auth_third_party_done'] = 1
|
||||
return Response({"msg": "ok"})
|
||||
except errors.LoginConfirmOtherError as e:
|
||||
reason = e.msg
|
||||
username = e.username
|
||||
self.send_auth_signal(success=False, username=username, reason=reason)
|
||||
# 若为三方登录,此时应退出登录
|
||||
auth_logout(request)
|
||||
return Response(e.as_data(), status=200)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class JMSBaseAuthBackend:
|
||||
if not allow:
|
||||
info = 'User {} skip authentication backend {}, because it not in {}'
|
||||
info = info.format(username, backend_name, ','.join(allowed_backend_names))
|
||||
logger.debug(info)
|
||||
logger.info(info)
|
||||
return allow
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
from django.urls import path
|
||||
import django_cas_ng.views
|
||||
|
||||
from .views import CASLoginView
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
|
||||
path('login/', CASLoginView.as_view(), name='cas-login'),
|
||||
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
|
||||
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
|
||||
]
|
||||
|
||||
15
apps/authentication/backends/cas/views.py
Normal file
15
apps/authentication/backends/cas/views.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django_cas_ng.views import LoginView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
__all__ = ['LoginView']
|
||||
|
||||
|
||||
class CASLoginView(LoginView):
|
||||
def get(self, request):
|
||||
try:
|
||||
return super().get(request)
|
||||
except PermissionDenied:
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
|
||||
61
apps/authentication/backends/custom.py
Normal file
61
apps/authentication/backends/custom.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
from common.utils import get_logger
|
||||
from django.contrib.auth import get_user_model
|
||||
from authentication.signals import user_auth_failed, user_auth_success
|
||||
|
||||
from .base import JMSModelBackend
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
custom_authenticate_method = None
|
||||
|
||||
if settings.AUTH_CUSTOM:
|
||||
""" 保证自定义认证方法在服务运行时不能被更改,只在第一次调用时加载一次 """
|
||||
try:
|
||||
custom_auth_method_path = 'data.auth.main.authenticate'
|
||||
custom_authenticate_method = import_string(custom_auth_method_path)
|
||||
except Exception as e:
|
||||
logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e))
|
||||
|
||||
|
||||
class CustomAuthBackend(JMSModelBackend):
|
||||
|
||||
def is_enabled(self):
|
||||
return settings.AUTH_CUSTOM and callable(custom_authenticate_method)
|
||||
|
||||
@staticmethod
|
||||
def get_or_create_user_from_userinfo(userinfo: dict):
|
||||
username = userinfo['username']
|
||||
attrs = ['name', 'username', 'email', 'is_active']
|
||||
defaults = {attr: userinfo[attr] for attr in attrs}
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults=defaults
|
||||
)
|
||||
return user, created
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
try:
|
||||
userinfo: dict = custom_authenticate_method(
|
||||
username=username, password=password, **kwargs
|
||||
)
|
||||
user, created = self.get_or_create_user_from_userinfo(userinfo)
|
||||
except Exception as e:
|
||||
logger.error('Custom authenticate error: {}'.format(e))
|
||||
return None
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.info(f'Custom authenticate success: {user.username}')
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_CUSTOM
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.info(f'Custom authenticate failed: {user.username}')
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason=_('User invalid, disabled or expired'),
|
||||
backend=settings.AUTH_BACKEND_CUSTOM
|
||||
)
|
||||
return None
|
||||
4
apps/authentication/backends/oauth2/__init__.py
Normal file
4
apps/authentication/backends/oauth2/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .backends import *
|
||||
161
apps/authentication/backends/oauth2/backends.py
Normal file
161
apps/authentication/backends/oauth2/backends.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import requests
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.http import urlencode
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
from authentication.utils import build_absolute_uri
|
||||
from authentication.signals import user_auth_failed, user_auth_success
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .signals import (
|
||||
oauth2_create_or_update_user
|
||||
)
|
||||
from ..base import JMSModelBackend
|
||||
|
||||
|
||||
__all__ = ['OAuth2Backend']
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class OAuth2Backend(JMSModelBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_OAUTH2
|
||||
|
||||
def get_or_create_user_from_userinfo(self, request, userinfo):
|
||||
log_prompt = "Get or Create user [OAuth2Backend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
# Construct user attrs value
|
||||
user_attrs = {}
|
||||
for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items():
|
||||
user_attrs[field] = userinfo.get(attr, '')
|
||||
|
||||
username = user_attrs.get('username')
|
||||
if not username:
|
||||
error_msg = 'username is missing'
|
||||
logger.error(log_prompt.format(error_msg))
|
||||
raise JMSException(error_msg)
|
||||
|
||||
email = user_attrs.get('email', '')
|
||||
email = construct_user_email(user_attrs.get('username'), email)
|
||||
user_attrs.update({'email': email})
|
||||
|
||||
logger.debug(log_prompt.format(user_attrs))
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults=user_attrs
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => oauth2 create or update user"))
|
||||
oauth2_create_or_update_user.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
attrs=user_attrs
|
||||
)
|
||||
return user, created
|
||||
|
||||
@staticmethod
|
||||
def get_response_data(response_data):
|
||||
if response_data.get('data') is not None:
|
||||
response_data = response_data['data']
|
||||
return response_data
|
||||
|
||||
@staticmethod
|
||||
def get_query_dict(response_data, query_dict):
|
||||
query_dict.update({
|
||||
'uid': response_data.get('uid', ''),
|
||||
'access_token': response_data.get('access_token', '')
|
||||
})
|
||||
return query_dict
|
||||
|
||||
def authenticate(self, request, code=None, **kwargs):
|
||||
log_prompt = "Process authenticate [OAuth2Backend]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
if code is None:
|
||||
logger.error(log_prompt.format('code is missing'))
|
||||
return None
|
||||
|
||||
query_dict = {
|
||||
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
access_token_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict)
|
||||
)
|
||||
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
|
||||
requests_func = getattr(requests, token_method, requests.get)
|
||||
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
access_token_response = requests_func(access_token_url, headers=headers)
|
||||
try:
|
||||
access_token_response.raise_for_status()
|
||||
access_token_response_data = access_token_response.json()
|
||||
response_data = self.get_response_data(access_token_response_data)
|
||||
except Exception as e:
|
||||
error = "Json access token response error, access token response " \
|
||||
"content is: {}, error is: {}".format(access_token_response.content, str(e))
|
||||
logger.error(log_prompt.format(error))
|
||||
return None
|
||||
|
||||
query_dict = self.get_query_dict(response_data, query_dict)
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'token {}'.format(response_data.get('access_token', ''))
|
||||
}
|
||||
|
||||
logger.debug(log_prompt.format('Get userinfo endpoint'))
|
||||
userinfo_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT,
|
||||
query=urlencode(query_dict)
|
||||
)
|
||||
userinfo_response = requests.get(userinfo_url, headers=headers)
|
||||
try:
|
||||
userinfo_response.raise_for_status()
|
||||
userinfo_response_data = userinfo_response.json()
|
||||
if 'data' in userinfo_response_data:
|
||||
userinfo = userinfo_response_data['data']
|
||||
else:
|
||||
userinfo = userinfo_response_data
|
||||
except Exception as e:
|
||||
error = "Json userinfo response error, userinfo response " \
|
||||
"content is: {}, error is: {}".format(userinfo_response.content, str(e))
|
||||
logger.error(log_prompt.format(error))
|
||||
return None
|
||||
|
||||
try:
|
||||
logger.debug(log_prompt.format('Update or create oauth2 user'))
|
||||
user, created = self.get_or_create_user_from_userinfo(request, userinfo)
|
||||
except JMSException:
|
||||
return None
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OAuth2 user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => oauth2 user login success'))
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_OAUTH2
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OAuth2 user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason=_('User invalid, disabled or expired'),
|
||||
backend=settings.AUTH_BACKEND_OAUTH2
|
||||
)
|
||||
return None
|
||||
7
apps/authentication/backends/oauth2/signals.py
Normal file
7
apps/authentication/backends/oauth2/signals.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
oauth2_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
|
||||
12
apps/authentication/backends/oauth2/urls.py
Normal file
12
apps/authentication/backends/oauth2/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
|
||||
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
|
||||
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
|
||||
]
|
||||
90
apps/authentication/backends/oauth2/views.py
Normal file
90
apps/authentication/backends/oauth2/views.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from authentication.utils import build_absolute_uri
|
||||
from common.utils import get_logger
|
||||
from authentication.mixins import authenticate
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OAuth2AuthRequestView(View):
|
||||
|
||||
def get(self, request):
|
||||
log_prompt = "Process OAuth2 GET requests: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
query_dict = {
|
||||
'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
|
||||
'scope': settings.AUTH_OAUTH2_SCOPE,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
redirect_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
|
||||
query=urlencode(query_dict)
|
||||
)
|
||||
logger.debug(log_prompt.format('Redirect login url'))
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OAuth2AuthCallbackView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
callback_params = request.GET
|
||||
|
||||
if 'code' in callback_params:
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = authenticate(code=callback_params['code'], request=request)
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(
|
||||
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
|
||||
)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
# OAuth2 服务端认证成功, 但是用户被禁用了, 这时候需要调用服务端的logout
|
||||
redirect_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OAuth2EndSessionView(View):
|
||||
http_method_names = ['get', 'post', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OAuth2EndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
return self.post(request)
|
||||
|
||||
def post(self, request):
|
||||
""" Processes POST requests. """
|
||||
log_prompt = "Process POST requests [OAuth2EndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
|
||||
|
||||
# Log out the current user.
|
||||
if request.user.is_authenticated:
|
||||
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
|
||||
auth.logout(request)
|
||||
|
||||
if settings.AUTH_OAUTH2_LOGOUT_COMPLETELY:
|
||||
logger.debug(log_prompt.format('Log out OAUTH2 platform user session synchronously'))
|
||||
next_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(logout_url)
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from rest_framework.exceptions import ParseError
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
@@ -18,14 +19,16 @@ from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
from .utils import validate_and_return_id_token
|
||||
from .decorator import ssl_verification
|
||||
from .signals import (
|
||||
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
|
||||
openid_create_or_update_user
|
||||
)
|
||||
from authentication.signals import user_auth_success, user_auth_failed
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -127,7 +130,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
token_payload = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
'redirect_uri': build_absolute_uri_for_oidc(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
@@ -211,14 +214,18 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_OIDC_CODE
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason="User is invalid"
|
||||
reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE
|
||||
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -269,8 +276,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
"content is: {}, error is: {}".format(token_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error,
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return
|
||||
|
||||
@@ -297,8 +305,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error,
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return
|
||||
|
||||
@@ -310,13 +319,16 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(
|
||||
sender=self.__class__, request=request, user=user
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid"
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid",
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -13,6 +13,4 @@ from django.dispatch import Signal
|
||||
openid_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
openid_user_login_success = Signal(providing_args=['request', 'user'])
|
||||
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import datetime as dt
|
||||
from calendar import timegm
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_bytes, smart_bytes
|
||||
@@ -110,17 +110,3 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
|
||||
raise SuspiciousOperation('Incorrect id_token: nonce')
|
||||
|
||||
logger.debug(log_prompt.format('End'))
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
"""
|
||||
Build absolute redirect uri
|
||||
"""
|
||||
if path is None:
|
||||
path = '/'
|
||||
|
||||
if settings.BASE_SITE_URL:
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
else:
|
||||
redirect_uri = request.build_absolute_uri(path)
|
||||
return redirect_uri
|
||||
|
||||
@@ -20,7 +20,8 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.http import is_safe_url, urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from .utils import get_logger, build_absolute_uri
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from .utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -50,7 +51,7 @@ class OIDCAuthRequestView(View):
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
'response_type': 'code',
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
'redirect_uri': build_absolute_uri_for_oidc(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
})
|
||||
@@ -216,7 +217,7 @@ class OIDCEndSessionView(View):
|
||||
""" Returns the end-session URL. """
|
||||
q = QueryDict(mutable=True)
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
|
||||
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
|
||||
self.request.session['oidc_auth_id_token']
|
||||
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
|
||||
|
||||
@@ -7,9 +7,9 @@ from django.db import transaction
|
||||
from common.utils import get_logger
|
||||
from authentication.errors import reason_choices, reason_user_invalid
|
||||
from .signals import (
|
||||
saml2_user_authenticated, saml2_user_authentication_failed,
|
||||
saml2_create_or_update_user
|
||||
)
|
||||
from authentication.signals import user_auth_failed, user_auth_success
|
||||
from ..base import JMSModelBackend
|
||||
|
||||
__all__ = ['SAML2Backend']
|
||||
@@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend):
|
||||
return user, created
|
||||
|
||||
def authenticate(self, request, saml_user_data=None, **kwargs):
|
||||
log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
|
||||
log_prompt = "Process authenticate [SAML2Backend]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
if saml_user_data is None:
|
||||
logger.error(log_prompt.format('saml_user_data is missing'))
|
||||
@@ -48,21 +48,23 @@ class SAML2Backend(JMSModelBackend):
|
||||
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
|
||||
username = saml_user_data.get('username')
|
||||
if not username:
|
||||
logger.debug(log_prompt.format('username is missing'))
|
||||
logger.warning(log_prompt.format('username is missing'))
|
||||
return None
|
||||
|
||||
user, created = self.get_or_create_from_saml_data(request, **saml_user_data)
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('SAML2 user login success'))
|
||||
saml2_user_authenticated.send(
|
||||
sender=self, request=request, user=user, created=created
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
backend=settings.AUTH_BACKEND_SAML2
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('SAML2 user login failed'))
|
||||
saml2_user_authentication_failed.send(
|
||||
sender=self, request=request, username=username,
|
||||
reason=reason_choices.get(reason_user_invalid)
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username,
|
||||
reason=reason_choices.get(reason_user_invalid),
|
||||
backend=settings.AUTH_BACKEND_SAML2
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -2,5 +2,3 @@ from django.dispatch import Signal
|
||||
|
||||
|
||||
saml2_create_or_update_user = Signal(providing_args=('user', 'created', 'request', 'attrs'))
|
||||
saml2_user_authenticated = Signal(providing_args=('user', 'created', 'request'))
|
||||
saml2_user_authentication_failed = Signal(providing_args=('request', 'username', 'reason'))
|
||||
|
||||
@@ -3,7 +3,7 @@ import copy
|
||||
from urllib import parse
|
||||
|
||||
from django.views import View
|
||||
from django.contrib import auth as auth
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@@ -271,7 +271,10 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
|
||||
auth.login(self.request, user)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
next_url = saml_instance.redirect_to(post_data.get('RelayState', '/'))
|
||||
redir = post_data.get('RelayState')
|
||||
if not redir or len(redir) == 0:
|
||||
redir = "/"
|
||||
next_url = saml_instance.redirect_to(redir)
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
@csrf_exempt
|
||||
|
||||
@@ -12,12 +12,13 @@ class AuthFailedNeedLogMixin:
|
||||
username = ''
|
||||
request = None
|
||||
error = ''
|
||||
msg = ''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=self.username,
|
||||
request=self.request, reason=self.error
|
||||
request=self.request, reason=self.msg
|
||||
)
|
||||
|
||||
|
||||
@@ -55,7 +56,8 @@ class BlockGlobalIpLoginError(AuthFailedError):
|
||||
error = 'block_global_ip_login'
|
||||
|
||||
def __init__(self, username, ip, **kwargs):
|
||||
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
|
||||
if not self.msg:
|
||||
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
|
||||
LoginIpBlockUtil(ip).set_block_if_need()
|
||||
super().__init__(username=username, ip=ip, **kwargs)
|
||||
|
||||
@@ -65,22 +67,21 @@ class CredentialError(
|
||||
BlockGlobalIpLoginError, AuthFailedError
|
||||
):
|
||||
def __init__(self, error, username, ip, request):
|
||||
super().__init__(error=error, username=username, ip=ip, request=request)
|
||||
util = LoginBlockUtil(username, ip)
|
||||
times_remainder = util.get_remainder_times()
|
||||
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
|
||||
|
||||
if times_remainder < 1:
|
||||
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
|
||||
return
|
||||
|
||||
default_msg = const.invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if error == const.reason_password_failed:
|
||||
self.msg = default_msg
|
||||
else:
|
||||
self.msg = const.reason_choices.get(error, default_msg)
|
||||
default_msg = const.invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if error == const.reason_password_failed:
|
||||
self.msg = default_msg
|
||||
else:
|
||||
self.msg = const.reason_choices.get(error, default_msg)
|
||||
# 先处理 msg 在 super,记录日志时原因才准确
|
||||
super().__init__(error=error, username=username, ip=ip, request=request)
|
||||
|
||||
|
||||
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
@@ -138,18 +139,11 @@ class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
}
|
||||
|
||||
|
||||
class LoginIPNotAllowed(ACLError):
|
||||
class LoginACLIPAndTimePeriodNotAllowed(ACLError):
|
||||
def __init__(self, username, request, **kwargs):
|
||||
self.username = username
|
||||
self.request = request
|
||||
super().__init__(_("IP is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class TimePeriodNotAllowed(ACLError):
|
||||
def __init__(self, username, request, **kwargs):
|
||||
self.username = username
|
||||
self.request = request
|
||||
super().__init__(_("Time Period is not allowed"), **kwargs)
|
||||
super().__init__(_("Current IP and Time period is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class MFACodeRequiredError(AuthFailedError):
|
||||
|
||||
@@ -69,10 +69,16 @@ class LoginConfirmWaitError(LoginConfirmBaseError):
|
||||
class LoginConfirmOtherError(LoginConfirmBaseError):
|
||||
error = 'login_confirm_error'
|
||||
|
||||
def __init__(self, ticket_id, status):
|
||||
def __init__(self, ticket_id, status, username):
|
||||
self.username = username
|
||||
msg = const.login_confirm_error_msg.format(status)
|
||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||
|
||||
def as_data(self):
|
||||
ret = super().as_data()
|
||||
ret['data']['username'] = self.username
|
||||
return ret
|
||||
|
||||
|
||||
class PasswordTooSimple(NeedRedirectError):
|
||||
default_code = 'passwd_too_simple'
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import base64
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.shortcuts import redirect, reverse, render
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
from apps.authentication import mixins
|
||||
from common.utils import gen_key_pair
|
||||
from common.utils import get_request_ip
|
||||
from .signals import post_auth_failed
|
||||
|
||||
|
||||
class MFAMiddleware:
|
||||
@@ -13,6 +18,7 @@ class MFAMiddleware:
|
||||
这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS,使用第三方库做的登录,直接 login 了,
|
||||
所以只能在 Middleware 中控制
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -42,6 +48,50 @@ class MFAMiddleware:
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class ThirdPartyLoginMiddleware(mixins.AuthMixin):
|
||||
"""OpenID、CAS、SAML2登录规则设置验证"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
# 没有认证过,证明不是从 第三方 来的
|
||||
if request.user.is_anonymous:
|
||||
return response
|
||||
if not request.session.get('auth_third_party_required'):
|
||||
return response
|
||||
ip = get_request_ip(request)
|
||||
try:
|
||||
self.request = request
|
||||
self._check_login_acl(request.user, ip)
|
||||
except Exception as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=request.user.username,
|
||||
request=self.request, reason=e.msg
|
||||
)
|
||||
auth_logout(request)
|
||||
context = {
|
||||
'title': _('Authentication failed'),
|
||||
'message': _('Authentication failed (before login check failed): {}').format(e),
|
||||
'interval': 10,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context)
|
||||
else:
|
||||
if not self.request.session['auth_confirm_required']:
|
||||
return response
|
||||
guard_url = reverse('authentication:login-guard')
|
||||
args = request.META.get('QUERY_STRING', '')
|
||||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
response = redirect(guard_url)
|
||||
finally:
|
||||
request.session.pop('auth_third_party_required', '')
|
||||
return response
|
||||
|
||||
|
||||
class SessionCookieMiddleware(MiddlewareMixin):
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -328,13 +328,59 @@ class AuthACLMixin:
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
|
||||
if is_allowed:
|
||||
acl = LoginACL.match(user, ip)
|
||||
if not acl:
|
||||
return
|
||||
if limit_type == 'ip':
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
elif limit_type == 'time':
|
||||
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
|
||||
|
||||
acl: LoginACL
|
||||
if acl.is_action(acl.ActionChoices.allow):
|
||||
return
|
||||
|
||||
if acl.is_action(acl.ActionChoices.reject):
|
||||
raise errors.LoginACLIPAndTimePeriodNotAllowed(user.username, request=self.request)
|
||||
|
||||
if acl.is_action(acl.ActionChoices.confirm):
|
||||
self.request.session['auth_confirm_required'] = '1'
|
||||
self.request.session['auth_acl_id'] = str(acl.id)
|
||||
return
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
if not self.request.session.get("auth_confirm_required"):
|
||||
return
|
||||
acl_id = self.request.session.get('auth_acl_id')
|
||||
logger.debug('Login confirm acl id: {}'.format(acl_id))
|
||||
if not acl_id:
|
||||
return
|
||||
acl = LoginACL.filter_acl(user).filter(id=acl_id).first()
|
||||
if not acl:
|
||||
return
|
||||
if not acl.is_action(acl.ActionChoices.confirm):
|
||||
return
|
||||
self.get_ticket_or_create(acl)
|
||||
self.check_user_login_confirm()
|
||||
|
||||
def get_ticket_or_create(self, acl):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.is_state(ticket.State.closed):
|
||||
ticket = acl.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
||||
def check_user_login_confirm(self):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm_required"] = ''
|
||||
return
|
||||
elif ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
else:
|
||||
# rejected, closed
|
||||
ticket_id = ticket.id
|
||||
status = ticket.get_state_display()
|
||||
username = ticket.applicant.username
|
||||
raise errors.LoginConfirmOtherError(ticket_id, status, username)
|
||||
|
||||
def get_ticket(self):
|
||||
from tickets.models import ApplyLoginTicket
|
||||
@@ -346,44 +392,6 @@ class AuthACLMixin:
|
||||
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
|
||||
return ticket
|
||||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.is_status(ticket.Status.closed):
|
||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
||||
def check_user_login_confirm(self):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
|
||||
if ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return
|
||||
elif ticket.is_state(ticket.State.rejected):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
elif ticket.is_state(ticket.State.closed):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
else:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_status_display()
|
||||
)
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
ip = self.get_request_ip()
|
||||
is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip)
|
||||
if self.request.session.get('auth_confirm') or not is_allowed:
|
||||
return
|
||||
self.get_ticket_or_create(confirm_setting)
|
||||
self.check_user_login_confirm()
|
||||
|
||||
|
||||
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
|
||||
request = None
|
||||
@@ -482,7 +490,9 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
return self.check_user_auth(valid_data)
|
||||
|
||||
def clear_auth_mark(self):
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
keys = [
|
||||
'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id'
|
||||
]
|
||||
for k in keys:
|
||||
self.request.session.pop(k, '')
|
||||
|
||||
|
||||
@@ -6,13 +6,8 @@ from django.core.cache import cache
|
||||
from django.dispatch import receiver
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from authentication.backends.oidc.signals import (
|
||||
openid_user_login_failed, openid_user_login_success
|
||||
)
|
||||
from authentication.backends.saml2.signals import (
|
||||
saml2_user_authenticated, saml2_user_authentication_failed
|
||||
)
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
|
||||
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
@@ -25,7 +20,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
and user.mfa_enabled \
|
||||
and not request.session.get('auth_mfa'):
|
||||
request.session['auth_mfa_required'] = 1
|
||||
|
||||
if not request.session.get("auth_third_party_done") and \
|
||||
request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
|
||||
request.session['auth_third_party_required'] = 1
|
||||
# 单点登录,超过了自动退出
|
||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||
lock_key = 'single_machine_login_' + str(user.id)
|
||||
@@ -39,31 +36,19 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
request.session['auth_session_expiration_required'] = 1
|
||||
|
||||
|
||||
@receiver(openid_user_login_success)
|
||||
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(openid_user_login_failed)
|
||||
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
||||
|
||||
@receiver(cas_user_authenticated)
|
||||
def on_cas_user_login_success(sender, request, user, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_CAS
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(saml2_user_authenticated)
|
||||
def on_saml2_user_login_success(sender, request, user, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
|
||||
@receiver(user_auth_success)
|
||||
def on_user_login_success(sender, request, user, backend, create=False, **kwargs):
|
||||
request.session['auth_backend'] = backend
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(saml2_user_authentication_failed)
|
||||
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
|
||||
@receiver(user_auth_failed)
|
||||
def on_user_login_failed(sender, username, request, reason, backend, **kwargs):
|
||||
request.session['auth_backend'] = backend
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
||||
@@ -3,3 +3,7 @@ from django.dispatch import Signal
|
||||
|
||||
post_auth_success = Signal(providing_args=('user', 'request'))
|
||||
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))
|
||||
|
||||
|
||||
user_auth_success = Signal(providing_args=('user', 'request', 'backend', 'create'))
|
||||
user_auth_failed = Signal(providing_args=('username', 'request', 'reason', 'backend'))
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends '_base_only_content.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block html_title %} {{ title }} {% endblock %}
|
||||
{% block title %} {{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.alert.alert-msg {
|
||||
background: #F5F5F7;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<p>
|
||||
<div class="alert alert-msg" id="messages">
|
||||
{% if error %}
|
||||
{{ error }}
|
||||
{% else %}
|
||||
{{ message|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
{% if has_cancel %}
|
||||
<div class="col-sm-3">
|
||||
<a href="{{ cancel_url }}" class="btn btn-default block full-width m-b">
|
||||
{% trans 'Cancel' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-3">
|
||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
|
||||
{% if confirm_button %}
|
||||
{{ confirm_button }}
|
||||
{% else %}
|
||||
{% trans 'Confirm' %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var message = ''
|
||||
var time = '{{ interval }}'
|
||||
{% if error %}
|
||||
message = '{{ error }}'
|
||||
{% else %}
|
||||
message = '{{ message|safe }}'
|
||||
{% endif %}
|
||||
|
||||
function redirect_page() {
|
||||
if (time >= 0) {
|
||||
var msg = message + ', <b>' + time + '</b> ...';
|
||||
$('#messages').html(msg);
|
||||
time--;
|
||||
setTimeout(redirect_page, 1000);
|
||||
} else {
|
||||
window.location.href = "{{ redirect_url }}";
|
||||
}
|
||||
}
|
||||
{% if auto_redirect %}
|
||||
window.onload = redirect_page;
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -79,6 +79,9 @@ function doRequestAuth() {
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-JMS-LOGIN-TYPE": "W"
|
||||
},
|
||||
success: function (data) {
|
||||
if (!data.error && data.msg === 'ok') {
|
||||
window.onbeforeunload = function(){};
|
||||
@@ -98,7 +101,7 @@ function doRequestAuth() {
|
||||
},
|
||||
error: function (text, data) {
|
||||
},
|
||||
flash_message: false
|
||||
flash_message: false, // 是否显示flash消息
|
||||
})
|
||||
}
|
||||
function initClipboard() {
|
||||
|
||||
@@ -56,9 +56,11 @@ urlpatterns = [
|
||||
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
|
||||
name='user-otp-disable'),
|
||||
|
||||
# openid
|
||||
# other authentication protocol
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
|
||||
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
|
||||
path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')),
|
||||
|
||||
path('captcha/', include('captcha.urls')),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import ipaddress
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
@@ -22,10 +25,34 @@ def check_different_city_login_if_need(user, request):
|
||||
else:
|
||||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
|
||||
city_white = ['LAN', ]
|
||||
if city not in city_white:
|
||||
city_white = [_('LAN'), 'LAN']
|
||||
is_private = ipaddress.ip_address(ip).is_private
|
||||
if not is_private:
|
||||
last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \
|
||||
.filter(username=user.username, status=True).first()
|
||||
|
||||
if last_user_login and last_user_login.city != city:
|
||||
DifferentCityLoginMessage(user, ip, city).publish_async()
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
""" Build absolute redirect """
|
||||
if path is None:
|
||||
path = '/'
|
||||
site_url = urlparse(settings.SITE_URL)
|
||||
scheme = site_url.scheme or request.scheme
|
||||
host = request.get_host()
|
||||
url = f'{scheme}://{host}'
|
||||
redirect_uri = urljoin(url, path)
|
||||
return redirect_uri
|
||||
|
||||
|
||||
def build_absolute_uri_for_oidc(request, path=None):
|
||||
""" Build absolute redirect uri for OIDC """
|
||||
if path is None:
|
||||
path = '/'
|
||||
if settings.BASE_SITE_URL:
|
||||
# OIDC 专用配置项
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
return redirect_uri
|
||||
return build_absolute_uri(request, path=path)
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import FlashMessageUtil
|
||||
from common.utils import FlashMessageUtil, static_or_direct
|
||||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
@@ -39,8 +39,7 @@ class UserLoginContextMixin:
|
||||
get_user_mfa_context: Callable
|
||||
request: HttpRequest
|
||||
|
||||
@staticmethod
|
||||
def get_support_auth_methods():
|
||||
def get_support_auth_methods(self):
|
||||
auth_methods = [
|
||||
{
|
||||
'name': 'OpenID',
|
||||
@@ -63,6 +62,13 @@ class UserLoginContextMixin:
|
||||
'logo': static('img/login_saml2_logo.png'),
|
||||
'auto_redirect': True
|
||||
},
|
||||
{
|
||||
'name': settings.AUTH_OAUTH2_PROVIDER,
|
||||
'enabled': settings.AUTH_OAUTH2,
|
||||
'url': reverse('authentication:oauth2:login'),
|
||||
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
|
||||
'auto_redirect': True
|
||||
},
|
||||
{
|
||||
'name': _('WeCom'),
|
||||
'enabled': settings.AUTH_WECOM,
|
||||
@@ -324,6 +330,8 @@ class UserLogoutView(TemplateView):
|
||||
return settings.CAS_LOGOUT_URL_NAME
|
||||
elif 'saml2' in backend:
|
||||
return settings.SAML2_LOGOUT_URL_NAME
|
||||
elif 'oauth2' in backend:
|
||||
return settings.AUTH_OAUTH2_LOGOUT_URL_NAME
|
||||
return None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -10,5 +10,5 @@ celery_task_pre_key = "CELERY_"
|
||||
KEY_CACHE_RESOURCE_IDS = "RESOURCE_IDS_{}"
|
||||
|
||||
# AD User AccountDisable
|
||||
# https://blog.csdn.net/bytxl/article/details/17763975
|
||||
# https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
|
||||
LDAP_AD_ACCOUNT_DISABLE = 2
|
||||
|
||||
@@ -67,7 +67,7 @@ class SimpleMetadataWithFilters(SimpleMetadata):
|
||||
|
||||
default = getattr(field, 'default', None)
|
||||
if default is not None and default != empty:
|
||||
if isinstance(default, (str, int, bool, datetime.datetime, list)):
|
||||
if isinstance(default, (str, int, bool, float, datetime.datetime, list)):
|
||||
field_info['default'] = default
|
||||
|
||||
for attr in self.attrs:
|
||||
|
||||
1
apps/common/hashers/__init__.py
Normal file
1
apps/common/hashers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .sm3 import PBKDF2SM3PasswordHasher
|
||||
23
apps/common/hashers/sm3.py
Normal file
23
apps/common/hashers/sm3.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from gmssl import sm3, func
|
||||
|
||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||
|
||||
|
||||
class Hasher:
|
||||
name = 'sm3'
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def hexdigest(self):
|
||||
return sm3.sm3_hash(func.bytes_to_list(self.key))
|
||||
|
||||
@staticmethod
|
||||
def hash(msg):
|
||||
return Hasher(msg)
|
||||
|
||||
|
||||
class PBKDF2SM3PasswordHasher(PBKDF2PasswordHasher):
|
||||
algorithm = "pbkdf2_sm3"
|
||||
digest = Hasher.hash
|
||||
|
||||
@@ -30,7 +30,9 @@ class CeleryBaseService(BaseService):
|
||||
'-l', 'INFO',
|
||||
'-c', str(self.num),
|
||||
'-Q', self.queue,
|
||||
'-n', f'{self.queue}@{server_hostname}'
|
||||
'--heartbeat-interval', '10',
|
||||
'-n', f'{self.queue}@{server_hostname}',
|
||||
'--without-mingle',
|
||||
]
|
||||
return cmd
|
||||
|
||||
|
||||
@@ -19,11 +19,13 @@ class FlowerService(BaseService):
|
||||
'celery',
|
||||
'-A', 'ops',
|
||||
'flower',
|
||||
'-l', 'INFO',
|
||||
'-logging=info',
|
||||
'--url_prefix=/core/flower',
|
||||
'--auto_refresh=False',
|
||||
'--max_tasks=1000',
|
||||
'--tasks_columns=uuid,name,args,state,received,started,runtime,worker'
|
||||
'--persistent=True',
|
||||
'-db=/opt/jumpserver/data/flower.db',
|
||||
'--state_save_interval=600000'
|
||||
]
|
||||
return cmd
|
||||
|
||||
|
||||
@@ -62,15 +62,22 @@ class UserConfirmation(permissions.BasePermission):
|
||||
|
||||
confirm_level = request.session.get('CONFIRM_LEVEL')
|
||||
confirm_time = request.session.get('CONFIRM_TIME')
|
||||
|
||||
ttl = self.get_ttl()
|
||||
if not confirm_level or not confirm_time or \
|
||||
confirm_level < self.min_level or \
|
||||
confirm_time < time.time() - self.ttl:
|
||||
confirm_time < time.time() - ttl:
|
||||
raise UserConfirmRequired(code=self.confirm_type)
|
||||
return True
|
||||
|
||||
def get_ttl(self):
|
||||
if self.confirm_type == ConfirmType.MFA:
|
||||
ttl = settings.SECURITY_MFA_VERIFY_TTL
|
||||
else:
|
||||
ttl = self.ttl
|
||||
return ttl
|
||||
|
||||
@classmethod
|
||||
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=300):
|
||||
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=60 * 5):
|
||||
min_level = ConfirmType.values.index(confirm_type) + 1
|
||||
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
|
||||
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
|
||||
|
||||
0
apps/common/sdk/gm/__init__.py
Normal file
0
apps/common/sdk/gm/__init__.py
Normal file
7
apps/common/sdk/gm/piico/__init__.py
Normal file
7
apps/common/sdk/gm/piico/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .device import Device
|
||||
|
||||
|
||||
def open_piico_device(driver_path) -> Device:
|
||||
d = Device()
|
||||
d.open(driver_path)
|
||||
return d
|
||||
59
apps/common/sdk/gm/piico/cipher.py
Normal file
59
apps/common/sdk/gm/piico/cipher.py
Normal file
@@ -0,0 +1,59 @@
|
||||
cipher_alg_id = {
|
||||
"sm4_ebc": 0x00000401,
|
||||
"sm4_cbc": 0x00000402,
|
||||
}
|
||||
|
||||
|
||||
class ECCCipher:
|
||||
def __init__(self, session, public_key, private_key):
|
||||
self._session = session
|
||||
self.public_key = public_key
|
||||
self.private_key = private_key
|
||||
|
||||
def encrypt(self, plain_text):
|
||||
return self._session.ecc_encrypt(self.public_key, plain_text, 0x00020800)
|
||||
|
||||
def decrypt(self, cipher_text):
|
||||
return self._session.ecc_decrypt(self.private_key, cipher_text, 0x00020800)
|
||||
|
||||
|
||||
class EBCCipher:
|
||||
|
||||
def __init__(self, session, key_val):
|
||||
self._session = session
|
||||
self._key = self.__get_key(key_val)
|
||||
self._alg = "sm4_ebc"
|
||||
self._iv = None
|
||||
|
||||
def __get_key(self, key_val):
|
||||
key_val = self.__padding(key_val)
|
||||
return self._session.import_key(key_val)
|
||||
|
||||
@staticmethod
|
||||
def __padding(val):
|
||||
# padding
|
||||
val = bytes(val)
|
||||
while len(val) == 0 or len(val) % 16 != 0:
|
||||
val += b'\0'
|
||||
return val
|
||||
|
||||
def encrypt(self, plain_text):
|
||||
plain_text = self.__padding(plain_text)
|
||||
cipher_text = self._session.encrypt(plain_text, self._key, cipher_alg_id[self._alg], self._iv)
|
||||
return bytes(cipher_text)
|
||||
|
||||
def decrypt(self, cipher_text):
|
||||
plain_text = self._session.decrypt(cipher_text, self._key, cipher_alg_id[self._alg], self._iv)
|
||||
return bytes(plain_text)
|
||||
|
||||
def destroy(self):
|
||||
self._session.destroy_cipher_key(self._key)
|
||||
self._session.close()
|
||||
|
||||
|
||||
class CBCCipher(EBCCipher):
|
||||
|
||||
def __init__(self, session, key, iv):
|
||||
super().__init__(session, key)
|
||||
self._iv = iv
|
||||
self._alg = "sm4_cbc"
|
||||
70
apps/common/sdk/gm/piico/device.py
Normal file
70
apps/common/sdk/gm/piico/device.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from ctypes import *
|
||||
|
||||
from .exception import PiicoError
|
||||
from .session import Session
|
||||
from .cipher import *
|
||||
from .digest import *
|
||||
|
||||
|
||||
class Device:
|
||||
_driver = None
|
||||
__device = None
|
||||
|
||||
def open(self, driver_path="./libpiico_ccmu.so"):
|
||||
# load driver
|
||||
self.__load_driver(driver_path)
|
||||
# open device
|
||||
self.__open_device()
|
||||
|
||||
def close(self):
|
||||
if self.__device is None:
|
||||
raise Exception("device not turned on")
|
||||
ret = self._driver.SDF_CloseDevice(self.__device)
|
||||
if ret != 0:
|
||||
raise Exception("turn off device failed")
|
||||
self.__device = None
|
||||
|
||||
def new_session(self):
|
||||
session = c_void_p()
|
||||
ret = self._driver.SDF_OpenSession(self.__device, pointer(session))
|
||||
if ret != 0:
|
||||
raise Exception("create session failed")
|
||||
return Session(self._driver, session)
|
||||
|
||||
def generate_ecc_key_pair(self):
|
||||
session = self.new_session()
|
||||
return session.generate_ecc_key_pair(alg_id=0x00020200)
|
||||
|
||||
def generate_random(self, length=64):
|
||||
session = self.new_session()
|
||||
return session.generate_random(length)
|
||||
|
||||
def new_sm2_ecc_cipher(self, public_key, private_key):
|
||||
session = self.new_session()
|
||||
return ECCCipher(session, public_key, private_key)
|
||||
|
||||
def new_sm4_ebc_cipher(self, key_val):
|
||||
session = self.new_session()
|
||||
return EBCCipher(session, key_val)
|
||||
|
||||
def new_sm4_cbc_cipher(self, key_val, iv):
|
||||
session = self.new_session()
|
||||
return CBCCipher(session, key_val, iv)
|
||||
|
||||
def new_digest(self, mode="sm3"):
|
||||
session = self.new_session()
|
||||
return Digest(session, mode)
|
||||
|
||||
def __load_driver(self, path):
|
||||
# check driver status
|
||||
if self._driver is not None:
|
||||
raise Exception("already load driver")
|
||||
# load driver
|
||||
self._driver = cdll.LoadLibrary(path)
|
||||
|
||||
def __open_device(self):
|
||||
device = c_void_p()
|
||||
ret = self._driver.SDF_OpenDevice(pointer(device))
|
||||
if ret != 0:
|
||||
raise PiicoError("open piico device failed", ret)
|
||||
self.__device = device
|
||||
32
apps/common/sdk/gm/piico/digest.py
Normal file
32
apps/common/sdk/gm/piico/digest.py
Normal file
@@ -0,0 +1,32 @@
|
||||
hash_alg_id = {
|
||||
"sm3": 0x00000001,
|
||||
"sha1": 0x00000002,
|
||||
"sha256": 0x00000004,
|
||||
"sha512": 0x00000008,
|
||||
}
|
||||
|
||||
|
||||
class Digest:
|
||||
|
||||
def __init__(self, session, alg_name="sm3"):
|
||||
if hash_alg_id[alg_name] is None:
|
||||
raise Exception("unsupported hash alg {}".format(alg_name))
|
||||
|
||||
self._alg_name = alg_name
|
||||
self._session = session
|
||||
self.__init_hash()
|
||||
|
||||
def __init_hash(self):
|
||||
self._session.hash_init(hash_alg_id[self._alg_name])
|
||||
|
||||
def update(self, data):
|
||||
self._session.hash_update(data)
|
||||
|
||||
def final(self):
|
||||
return self._session.hash_final()
|
||||
|
||||
def reset(self):
|
||||
self.__init_hash()
|
||||
|
||||
def destroy(self):
|
||||
self._session.close()
|
||||
71
apps/common/sdk/gm/piico/ecc.py
Normal file
71
apps/common/sdk/gm/piico/ecc.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from ctypes import *
|
||||
|
||||
ECCref_MAX_BITS = 512
|
||||
ECCref_MAX_LEN = int((ECCref_MAX_BITS + 7) / 8)
|
||||
|
||||
|
||||
class EncodeMixin:
|
||||
def encode(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ECCrefPublicKey(Structure, EncodeMixin):
|
||||
_fields_ = [
|
||||
('bits', c_uint),
|
||||
('x', c_ubyte * ECCref_MAX_LEN),
|
||||
('y', c_ubyte * ECCref_MAX_LEN),
|
||||
]
|
||||
|
||||
def encode(self):
|
||||
return bytes([0x04]) + bytes(self.x[32:]) + bytes(self.y[32:])
|
||||
|
||||
|
||||
class ECCrefPrivateKey(Structure, EncodeMixin):
|
||||
_fields_ = [
|
||||
('bits', c_uint,),
|
||||
('K', c_ubyte * ECCref_MAX_LEN),
|
||||
]
|
||||
|
||||
def encode(self):
|
||||
return bytes(self.K[32:])
|
||||
|
||||
|
||||
class ECCCipherEncode(EncodeMixin):
|
||||
|
||||
def __init__(self):
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.M = None
|
||||
self.C = None
|
||||
self.L = None
|
||||
|
||||
def encode(self):
|
||||
c1 = bytes(self.x[32:]) + bytes(self.y[32:])
|
||||
c2 = bytes(self.C[:self.L])
|
||||
c3 = bytes(self.M)
|
||||
return bytes([0x04]) + c1 + c2 + c3
|
||||
|
||||
|
||||
def new_ecc_cipher_cla(length):
|
||||
_cache = {}
|
||||
cla_name = "ECCCipher{}".format(length)
|
||||
if _cache.__contains__(cla_name):
|
||||
return _cache[cla_name]
|
||||
else:
|
||||
cla = type(cla_name, (Structure, ECCCipherEncode), {
|
||||
"_fields_": [
|
||||
('x', c_ubyte * ECCref_MAX_LEN),
|
||||
('y', c_ubyte * ECCref_MAX_LEN),
|
||||
('M', c_ubyte * 32),
|
||||
('L', c_uint),
|
||||
('C', c_ubyte * length)
|
||||
]
|
||||
})
|
||||
_cache[cla_name] = cla
|
||||
return cla
|
||||
|
||||
|
||||
class ECCKeyPair:
|
||||
def __init__(self, public_key, private_key):
|
||||
self.public_key = public_key
|
||||
self.private_key = private_key
|
||||
12
apps/common/sdk/gm/piico/exception.py
Normal file
12
apps/common/sdk/gm/piico/exception.py
Normal file
@@ -0,0 +1,12 @@
|
||||
class PiicoError(Exception):
|
||||
def __init__(self, msg, ret):
|
||||
super().__init__(self)
|
||||
self.__ret = ret
|
||||
self.__msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return "piico error: {} return code: {}".format(self.__msg, self.hex_ret(self.__ret))
|
||||
|
||||
@staticmethod
|
||||
def hex_ret(ret):
|
||||
return hex(ret & ((1 << 32) - 1))
|
||||
36
apps/common/sdk/gm/piico/session.py
Normal file
36
apps/common/sdk/gm/piico/session.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from ctypes import *
|
||||
|
||||
from .ecc import ECCrefPublicKey, ECCrefPrivateKey, ECCKeyPair
|
||||
from .exception import PiicoError
|
||||
from .session_mixin import SM3Mixin, SM4Mixin, SM2Mixin
|
||||
|
||||
|
||||
class Session(SM2Mixin, SM3Mixin, SM4Mixin):
|
||||
def __init__(self, driver, session):
|
||||
super().__init__()
|
||||
self._session = session
|
||||
self._driver = driver
|
||||
|
||||
def get_device_info(self):
|
||||
pass
|
||||
|
||||
def generate_random(self, length=64):
|
||||
random_data = (c_ubyte * length)()
|
||||
ret = self._driver.SDF_GenerateRandom(self._session, c_int(length), random_data)
|
||||
if ret != 0:
|
||||
raise PiicoError("generate random error", ret)
|
||||
return bytes(random_data)
|
||||
|
||||
def generate_ecc_key_pair(self, alg_id):
|
||||
public_key = ECCrefPublicKey()
|
||||
private_key = ECCrefPrivateKey()
|
||||
ret = self._driver.SDF_GenerateKeyPair_ECC(self._session, c_int(alg_id), c_int(256), pointer(public_key),
|
||||
pointer(private_key))
|
||||
if ret != 0:
|
||||
raise PiicoError("generate ecc key pair failed", ret)
|
||||
return ECCKeyPair(public_key.encode(), private_key.encode())
|
||||
|
||||
def close(self):
|
||||
ret = self._driver.SDF_CloseSession(self._session)
|
||||
if ret != 0:
|
||||
raise PiicoError("close session failed", ret)
|
||||
129
apps/common/sdk/gm/piico/session_mixin.py
Normal file
129
apps/common/sdk/gm/piico/session_mixin.py
Normal file
@@ -0,0 +1,129 @@
|
||||
|
||||
from .ecc import *
|
||||
from .exception import PiicoError
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
|
||||
def __init__(self):
|
||||
self._driver = None
|
||||
self._session = None
|
||||
|
||||
|
||||
class SM2Mixin(BaseMixin):
|
||||
def ecc_encrypt(self, public_key, plain_text, alg_id):
|
||||
|
||||
pos = 1
|
||||
k1 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
|
||||
k1 = (c_ubyte * len(k1))(*k1)
|
||||
pos += 32
|
||||
k2 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
|
||||
|
||||
pk = ECCrefPublicKey(c_uint(0x40), (c_ubyte * len(k1))(*k1), (c_ubyte * len(k2))(*k2))
|
||||
|
||||
plain_text = (c_ubyte * len(plain_text))(*plain_text)
|
||||
ecc_data = new_ecc_cipher_cla(len(plain_text))()
|
||||
ret = self._driver.SDF_ExternalEncrypt_ECC(self._session, c_int(alg_id), pointer(pk), plain_text,
|
||||
c_int(len(plain_text)), pointer(ecc_data))
|
||||
if ret != 0:
|
||||
raise Exception("ecc encrypt failed", ret)
|
||||
return ecc_data.encode()
|
||||
|
||||
def ecc_decrypt(self, private_key, cipher_text, alg_id):
|
||||
|
||||
k = bytes([0] * 32) + bytes(private_key[:32])
|
||||
vk = ECCrefPrivateKey(c_uint(0x40), (c_ubyte * len(k))(*k))
|
||||
|
||||
pos = 1
|
||||
# c1
|
||||
x = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
|
||||
pos += 32
|
||||
y = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
|
||||
pos += 32
|
||||
|
||||
# c2
|
||||
c = bytes(cipher_text[pos:-32])
|
||||
l = len(c)
|
||||
|
||||
# c3
|
||||
m = bytes(cipher_text[-32:])
|
||||
|
||||
ecc_data = new_ecc_cipher_cla(l)(
|
||||
(c_ubyte * 64)(*x),
|
||||
(c_ubyte * 64)(*y),
|
||||
(c_ubyte * 32)(*m),
|
||||
c_uint(l),
|
||||
(c_ubyte * l)(*c),
|
||||
)
|
||||
temp_data = (c_ubyte * l)()
|
||||
temp_data_length = c_int()
|
||||
ret = self._driver.SDF_ExternalDecrypt_ECC(self._session, c_int(alg_id), pointer(vk),
|
||||
pointer(ecc_data),
|
||||
temp_data, pointer(temp_data_length))
|
||||
if ret != 0:
|
||||
raise Exception("ecc decrypt failed", ret)
|
||||
return bytes(temp_data[:temp_data_length.value])
|
||||
|
||||
|
||||
class SM3Mixin(BaseMixin):
|
||||
def hash_init(self, alg_id):
|
||||
ret = self._driver.SDF_HashInit(self._session, c_int(alg_id), None, None, c_int(0))
|
||||
if ret != 0:
|
||||
raise PiicoError("hash init failed,alg id is {}".format(alg_id), ret)
|
||||
|
||||
def hash_update(self, data):
|
||||
data = (c_ubyte * len(data))(*data)
|
||||
ret = self._driver.SDF_HashUpdate(self._session, data, c_int(len(data)))
|
||||
if ret != 0:
|
||||
raise PiicoError("hash update failed", ret)
|
||||
|
||||
def hash_final(self):
|
||||
result_data = (c_ubyte * 32)()
|
||||
result_length = c_int()
|
||||
ret = self._driver.SDF_HashFinal(self._session, result_data, pointer(result_length))
|
||||
if ret != 0:
|
||||
raise PiicoError("hash final failed", ret)
|
||||
return bytes(result_data[:result_length.value])
|
||||
|
||||
|
||||
class SM4Mixin(BaseMixin):
|
||||
|
||||
def import_key(self, key_val):
|
||||
# to c lang
|
||||
key_val = (c_ubyte * len(key_val))(*key_val)
|
||||
|
||||
key = c_void_p()
|
||||
ret = self._driver.SDF_ImportKey(self._session, key_val, c_int(len(key_val)), pointer(key))
|
||||
if ret != 0:
|
||||
raise PiicoError("import key failed", ret)
|
||||
return key
|
||||
|
||||
def destroy_cipher_key(self, key):
|
||||
ret = self._driver.SDF_DestroyKey(self._session, key)
|
||||
if ret != 0:
|
||||
raise Exception("destroy key failed")
|
||||
|
||||
def encrypt(self, plain_text, key, alg, iv=None):
|
||||
return self.__do_cipher_action(plain_text, key, alg, iv, True)
|
||||
|
||||
def decrypt(self, cipher_text, key, alg, iv=None):
|
||||
return self.__do_cipher_action(cipher_text, key, alg, iv, False)
|
||||
|
||||
def __do_cipher_action(self, text, key, alg, iv=None, encrypt=True):
|
||||
text = (c_ubyte * len(text))(*text)
|
||||
if iv is not None:
|
||||
iv = (c_ubyte * len(iv))(*iv)
|
||||
|
||||
temp_data = (c_ubyte * len(text))()
|
||||
temp_data_length = c_int()
|
||||
if encrypt:
|
||||
ret = self._driver.SDF_Encrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
|
||||
pointer(temp_data_length))
|
||||
if ret != 0:
|
||||
raise PiicoError("encrypt failed", ret)
|
||||
else:
|
||||
ret = self._driver.SDF_Decrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
|
||||
pointer(temp_data_length))
|
||||
if ret != 0:
|
||||
raise PiicoError("decrypt failed", ret)
|
||||
return temp_data[:temp_data_length.value]
|
||||
0
apps/common/sdk/gm/piico/sign.py
Normal file
0
apps/common/sdk/gm/piico/sign.py
Normal file
@@ -17,4 +17,8 @@ class BaseSMSClient:
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def need_pre_check():
|
||||
return True
|
||||
|
||||
|
||||
|
||||
329
apps/common/sdk/sms/cmpp2.py
Normal file
329
apps/common/sdk/sms/cmpp2.py
Normal file
@@ -0,0 +1,329 @@
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from .base import BaseSMSClient
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
CMPP_CONNECT = 0x00000001 # 请求连接
|
||||
CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答
|
||||
CMPP_TERMINATE = 0x00000002 # 终止连接
|
||||
CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答
|
||||
CMPP_SUBMIT = 0x00000004 # 提交短信
|
||||
CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答
|
||||
CMPP_DELIVER = 0x00000005 # 短信下发
|
||||
CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答
|
||||
|
||||
|
||||
class CMPPBaseRequestInstance(object):
|
||||
def __init__(self):
|
||||
self.command_id = ''
|
||||
self.body = b''
|
||||
self.length = 0
|
||||
|
||||
def get_header(self, sequence_id):
|
||||
length = struct.pack('!L', 12 + self.length)
|
||||
command_id = struct.pack('!L', self.command_id)
|
||||
sequence_id = struct.pack('!L', sequence_id)
|
||||
return length + command_id + sequence_id
|
||||
|
||||
def get_message(self, sequence_id):
|
||||
return self.get_header(sequence_id) + self.body
|
||||
|
||||
|
||||
class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self, sp_id, sp_secret):
|
||||
if len(sp_id) != 6:
|
||||
raise ValueError(_("sp_id is 6 bits"))
|
||||
|
||||
super().__init__()
|
||||
|
||||
source_addr = sp_id.encode('utf-8')
|
||||
sp_secret = sp_secret.encode('utf-8')
|
||||
version = struct.pack('!B', 0x02)
|
||||
timestamp = struct.pack('!L', int(self.get_now()))
|
||||
authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
|
||||
auth_source_md5 = hashlib.md5(authenticator_source).digest()
|
||||
self.body = source_addr + auth_source_md5 + version + timestamp
|
||||
self.length = len(self.body)
|
||||
self.command_id = CMPP_CONNECT
|
||||
|
||||
@staticmethod
|
||||
def get_now():
|
||||
return time.strftime('%m%d%H%M%S', time.localtime(time.time()))
|
||||
|
||||
|
||||
class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
|
||||
service_id='', dest_usr_tl=1):
|
||||
if len(msg_content) >= 70:
|
||||
raise JMSException('The message length should be within 70 characters')
|
||||
if len(dest_terminal_id) > 100:
|
||||
raise JMSException('The number of users receiving information should be less than 100')
|
||||
|
||||
super().__init__()
|
||||
|
||||
msg_id = 8 * b'\x00'
|
||||
pk_total = struct.pack('!B', 1)
|
||||
pk_number = struct.pack('!B', 1)
|
||||
registered_delivery = struct.pack('!B', 0)
|
||||
msg_level = struct.pack('!B', 0)
|
||||
service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
|
||||
fee_user_type = struct.pack('!B', 2)
|
||||
fee_terminal_id = ('0' * 21).encode('utf-8')
|
||||
tp_pid = struct.pack('!B', 0)
|
||||
tp_udhi = struct.pack('!B', 0)
|
||||
msg_fmt = struct.pack('!B', 8)
|
||||
fee_type = '01'.encode('utf-8')
|
||||
fee_code = '000000'.encode('utf-8')
|
||||
valid_time = ('\x00' * 17).encode('utf-8')
|
||||
at_time = ('\x00' * 17).encode('utf-8')
|
||||
src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
|
||||
reserve = b'\x00' * 8
|
||||
_msg_length = struct.pack('!B', len(msg_content) * 2)
|
||||
_msg_src = msg_src.encode('utf-8')
|
||||
_dest_usr_tl = struct.pack('!B', dest_usr_tl)
|
||||
_msg_content = msg_content.encode('utf-16-be')
|
||||
_dest_terminal_id = b''.join([
|
||||
(i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
|
||||
])
|
||||
self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
|
||||
self.command_id = CMPP_SUBMIT
|
||||
self.body = msg_id + pk_total + pk_number + registered_delivery \
|
||||
+ msg_level + service_id + fee_user_type + fee_terminal_id \
|
||||
+ tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
|
||||
+ valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
|
||||
+ _msg_length + _msg_content + reserve
|
||||
|
||||
|
||||
class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.body = b''
|
||||
self.command_id = CMPP_TERMINATE
|
||||
|
||||
|
||||
class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self, msg_id, result=0):
|
||||
super().__init__()
|
||||
msg_id = struct.pack('!Q', msg_id)
|
||||
result = struct.pack('!B', result)
|
||||
self.length = len(self.body)
|
||||
self.body = msg_id + result
|
||||
|
||||
|
||||
class CMPPResponseInstance(object):
|
||||
def __init__(self):
|
||||
self.command_id = None
|
||||
self.length = None
|
||||
self.response_handler_map = {
|
||||
CMPP_CONNECT_RESP: self.connect_response_parse,
|
||||
CMPP_SUBMIT_RESP: self.submit_response_parse,
|
||||
CMPP_DELIVER: self.deliver_request_parse,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def connect_response_parse(body):
|
||||
status, = struct.unpack('!B', body[0:1])
|
||||
authenticator_ISMG = body[1:17]
|
||||
version, = struct.unpack('!B', body[17:18])
|
||||
return {
|
||||
'Status': status,
|
||||
'AuthenticatorISMG': authenticator_ISMG,
|
||||
'Version': version
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def submit_response_parse(body):
|
||||
msg_id = body[:8]
|
||||
result = struct.unpack('!B', body[8:9])
|
||||
return {
|
||||
'Msg_Id': msg_id, 'Result': result[0]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def deliver_request_parse(body):
|
||||
msg_id, = struct.unpack('!Q', body[0:8])
|
||||
dest_id = body[8:29]
|
||||
service_id = body[29:39]
|
||||
tp_pid = struct.unpack('!B', body[39:40])
|
||||
tp_udhi = struct.unpack('!B', body[40:41])
|
||||
msg_fmt = struct.unpack('!B', body[41:42])
|
||||
src_terminal_id = body[42:63]
|
||||
registered_delivery = struct.unpack('!B', body[63:64])
|
||||
msg_length = struct.unpack('!B', body[64:65])
|
||||
msg_content = body[65:msg_length[0]+65]
|
||||
return {
|
||||
'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
|
||||
'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
|
||||
'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
|
||||
'Msg_Length': msg_length, 'Msg_content': msg_content
|
||||
}
|
||||
|
||||
def parse_header(self, data):
|
||||
self.command_id, = struct.unpack('!L', data[4:8])
|
||||
sequence_id, = struct.unpack('!L', data[8:12])
|
||||
return {
|
||||
'length': self.length,
|
||||
'command_id': hex(self.command_id),
|
||||
'sequence_id': sequence_id
|
||||
}
|
||||
|
||||
def parse_body(self, body):
|
||||
response_body_func = self.response_handler_map.get(self.command_id)
|
||||
if response_body_func is None:
|
||||
raise JMSException('Unable to parse the returned result: %s' % body)
|
||||
return response_body_func(body)
|
||||
|
||||
def parse(self, data):
|
||||
self.length, = struct.unpack('!L', data[0:4])
|
||||
header = self.parse_header(data)
|
||||
body = self.parse_body(data[12:self.length])
|
||||
return header, body
|
||||
|
||||
|
||||
class CMPPClient(object):
|
||||
def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
|
||||
self.ip = host
|
||||
self.port = port
|
||||
self.sp_id = sp_id
|
||||
self.sp_secret = sp_secret
|
||||
self.src_id = src_id
|
||||
self.service_id = service_id
|
||||
self._sequence_id = 0
|
||||
self._is_connect = False
|
||||
self._times = 3
|
||||
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._connect()
|
||||
|
||||
@property
|
||||
def sequence_id(self):
|
||||
s = self._sequence_id
|
||||
self._sequence_id += 1
|
||||
return s
|
||||
|
||||
def _connect(self):
|
||||
self.__socket.settimeout(5)
|
||||
error_msg = _('Failed to connect to the CMPP gateway server, err: {}')
|
||||
for i in range(self._times):
|
||||
try:
|
||||
self.__socket.connect((self.ip, self.port))
|
||||
except Exception as err:
|
||||
error_msg = error_msg.format(str(err))
|
||||
logger.warning(error_msg)
|
||||
time.sleep(1)
|
||||
else:
|
||||
self._is_connect = True
|
||||
break
|
||||
else:
|
||||
raise JMSException(error_msg)
|
||||
|
||||
def send(self, instance):
|
||||
if isinstance(instance, CMPPBaseRequestInstance):
|
||||
message = instance.get_message(sequence_id=self.sequence_id)
|
||||
else:
|
||||
message = instance
|
||||
self.__socket.send(message)
|
||||
|
||||
def recv(self):
|
||||
raw_length = self.__socket.recv(4)
|
||||
length, = struct.unpack('!L', raw_length)
|
||||
header, body = CMPPResponseInstance().parse(
|
||||
raw_length + self.__socket.recv(length - 4)
|
||||
)
|
||||
return header, body
|
||||
|
||||
def close(self):
|
||||
if self._is_connect:
|
||||
terminate_request = CMPPTerminateRequestInstance()
|
||||
self.send(terminate_request)
|
||||
self.__socket.close()
|
||||
|
||||
def _cmpp_connect(self):
|
||||
connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
|
||||
self.send(connect_request)
|
||||
header, body = self.recv()
|
||||
if body['Status'] != 0:
|
||||
raise JMSException('CMPPv2.0 authentication failed: %s' % body)
|
||||
|
||||
def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
|
||||
"""
|
||||
优先发送template_param中message的信息
|
||||
若该内容不存在,则根据template_code构建验证码发送
|
||||
"""
|
||||
message = template_param.get('message')
|
||||
if message is None:
|
||||
code = template_param.get('code')
|
||||
message = template_code.replace('{code}', code)
|
||||
msg = '【%s】 %s' % (sign_name, message)
|
||||
submit_request = CMPPSubmitRequestInstance(
|
||||
msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
|
||||
dest_usr_tl=len(dest), dest_terminal_id=dest,
|
||||
service_id=self.service_id
|
||||
)
|
||||
self.send(submit_request)
|
||||
header, body = self.recv()
|
||||
command_id = header.get('command_id')
|
||||
if command_id == CMPP_DELIVER:
|
||||
deliver_request = CMPPDeliverRespRequestInstance(
|
||||
msg_id=body['Msg_Id'], result=body['Result']
|
||||
)
|
||||
self.send(deliver_request)
|
||||
|
||||
def send_sms(self, dest, sign_name, template_code, template_param):
|
||||
try:
|
||||
self._cmpp_connect()
|
||||
self._cmpp_send_sms(dest, sign_name, template_code, template_param)
|
||||
except Exception as e:
|
||||
logger.error('CMPPv2.0 Error: %s', e)
|
||||
self.close()
|
||||
raise JMSException(e)
|
||||
|
||||
|
||||
class CMPP2SMS(BaseSMSClient):
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
return cls(
|
||||
host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
|
||||
sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
|
||||
service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
|
||||
)
|
||||
|
||||
def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
|
||||
try:
|
||||
self.client = CMPPClient(
|
||||
host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
|
||||
)
|
||||
except Exception as err:
|
||||
self.client = None
|
||||
logger.warning(err)
|
||||
raise JMSException(err)
|
||||
|
||||
@staticmethod
|
||||
def need_pre_check():
|
||||
return False
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
try:
|
||||
logger.info(f'CMPPv2.0 sms send: '
|
||||
f'phone_numbers={phone_numbers} '
|
||||
f'sign_name={sign_name} '
|
||||
f'template_code={template_code} '
|
||||
f'template_param={template_param}')
|
||||
self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
|
||||
except Exception as e:
|
||||
raise JMSException(e)
|
||||
|
||||
|
||||
client = CMPP2SMS
|
||||
@@ -15,6 +15,8 @@ logger = get_logger(__name__)
|
||||
class BACKENDS(TextChoices):
|
||||
ALIBABA = 'alibaba', _('Alibaba cloud')
|
||||
TENCENT = 'tencent', _('Tencent cloud')
|
||||
HUAWEI = 'huawei', _('Huawei Cloud')
|
||||
CMPP2 = 'cmpp2', _('CMPP v2.0')
|
||||
|
||||
|
||||
class SMS:
|
||||
@@ -43,7 +45,7 @@ class SMS:
|
||||
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
|
||||
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
|
||||
|
||||
if not (sign_name and template_code):
|
||||
if self.client.need_pre_check() and not (sign_name and template_code):
|
||||
raise JMSException(
|
||||
code='verify_code_sign_tmpl_invalid',
|
||||
detail=_('SMS verification code signature or template invalid')
|
||||
|
||||
94
apps/common/sdk/sms/huawei.py
Normal file
94
apps/common/sdk/sms/huawei.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import get_logger
|
||||
|
||||
from .base import BaseSMSClient
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class HuaweiClient:
|
||||
def __init__(self, app_key, app_secret, url, sign_channel_num):
|
||||
self.url = url[:-1] if url.endswith('/') else url
|
||||
self.app_key = app_key
|
||||
self.app_secret = app_secret
|
||||
self.sign_channel_num = sign_channel_num
|
||||
|
||||
def build_wsse_header(self):
|
||||
now = time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
nonce = str(uuid.uuid4()).replace('-', '')
|
||||
digest = hashlib.sha256((nonce + now + self.app_secret).encode()).hexdigest()
|
||||
digestBase64 = base64.b64encode(digest.encode()).decode()
|
||||
formatter = 'UsernameToken Username="{}",PasswordDigest="{}",Nonce="{}",Created="{}"'
|
||||
return formatter.format(self.app_key, digestBase64, nonce, now)
|
||||
|
||||
def send_sms(self, receiver, signature, template_id, template_param):
|
||||
sms_url = '%s/%s' % (self.url, 'sms/batchSendSms/v1')
|
||||
headers = {
|
||||
'Authorization': 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"',
|
||||
'X-WSSE': self.build_wsse_header()
|
||||
}
|
||||
body = {
|
||||
'from': self.sign_channel_num, 'to': receiver, 'templateId': template_id,
|
||||
'templateParas': template_param, 'signature': signature
|
||||
}
|
||||
try:
|
||||
response = requests.post(sms_url, headers=headers, data=body)
|
||||
msg = response.json()
|
||||
except Exception as error:
|
||||
raise JMSException(code='response_bad', detail=error)
|
||||
return msg
|
||||
|
||||
|
||||
class HuaweiSMS(BaseSMSClient):
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'HUAWEI'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
return cls(
|
||||
app_key=settings.HUAWEI_APP_KEY,
|
||||
app_secret=settings.HUAWEI_APP_SECRET,
|
||||
url=settings.HUAWEI_SMS_ENDPOINT,
|
||||
sign_channel_num=settings.HUAWEI_SIGN_CHANNEL_NUM
|
||||
)
|
||||
|
||||
def __init__(self, app_key: str, app_secret: str, url: str, sign_channel_num: str):
|
||||
self.client = HuaweiClient(app_key, app_secret, url, sign_channel_num)
|
||||
|
||||
def send_sms(
|
||||
self, phone_numbers: list, sign_name: str, template_code: str,
|
||||
template_param: OrderedDict, **kwargs
|
||||
):
|
||||
phone_numbers_str = ','.join(phone_numbers)
|
||||
template_param = '["%s"]' % template_param.get('code')
|
||||
req_params = {
|
||||
'receiver': phone_numbers_str, 'signature': sign_name,
|
||||
'template_id': template_code, 'template_param': template_param
|
||||
}
|
||||
try:
|
||||
logger.info(f'Huawei sms send: '
|
||||
f'phone_numbers={phone_numbers} '
|
||||
f'sign_name={sign_name} '
|
||||
f'template_code={template_code} '
|
||||
f'template_param={template_param}')
|
||||
|
||||
resp_msg = self.client.send_sms(**req_params)
|
||||
|
||||
except Exception as error:
|
||||
raise JMSException(code='response_bad', detail=error)
|
||||
|
||||
if resp_msg.get('code') != '000000':
|
||||
raise JMSException(code='response_bad', detail=resp_msg)
|
||||
return resp_msg
|
||||
|
||||
|
||||
client = HuaweiSMS
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
import socket
|
||||
from django.templatetags.static import static
|
||||
from collections import OrderedDict
|
||||
from itertools import chain
|
||||
import logging
|
||||
@@ -365,3 +367,30 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
|
||||
|
||||
def group_by_count(it, count):
|
||||
return [it[i:i+count] for i in range(0, len(it), count)]
|
||||
|
||||
|
||||
def test_ip_connectivity(host, port, timeout=0.5):
|
||||
"""
|
||||
timeout: seconds
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
result = sock.connect_ex((host, int(port)))
|
||||
sock.close()
|
||||
if result == 0:
|
||||
connectivity = True
|
||||
else:
|
||||
connectivity = False
|
||||
return connectivity
|
||||
|
||||
|
||||
def static_or_direct(logo_path):
|
||||
if logo_path.startswith('img/'):
|
||||
return static(logo_path)
|
||||
else:
|
||||
return logo_path
|
||||
|
||||
|
||||
def make_dirs(name, mode=0o755, exist_ok=False):
|
||||
""" 默认权限设置为 0o755 """
|
||||
return os.makedirs(name, mode=mode, exist_ok=exist_ok)
|
||||
|
||||
@@ -53,7 +53,9 @@ class Subscription:
|
||||
error(msg, item)
|
||||
logger.error('Subscribe handler handle msg error: {}'.format(e))
|
||||
except Exception as e:
|
||||
logger.error('Consume msg error: {}'.format(e))
|
||||
# 正常的 websocket 断开时, redis 会断开连接,避免日志太多
|
||||
# logger.error('Consume msg error: {}'.format(e))
|
||||
pass
|
||||
|
||||
try:
|
||||
complete()
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
|
||||
from Cryptodome.Cipher import AES, PKCS1_v1_5
|
||||
from Cryptodome.Util.Padding import pad
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Util.Padding import pad
|
||||
from Cryptodome import Random
|
||||
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from common.sdk.gm import piico
|
||||
|
||||
def process_key(key):
|
||||
secret_pattern = re.compile(r'password|secret|key|token', re.IGNORECASE)
|
||||
|
||||
|
||||
def padding_key(key, max_length=32):
|
||||
"""
|
||||
返回32 bytes 的key
|
||||
"""
|
||||
if not isinstance(key, bytes):
|
||||
key = bytes(key, encoding='utf-8')
|
||||
|
||||
if len(key) >= 32:
|
||||
return key[:32]
|
||||
if len(key) >= max_length:
|
||||
return key[:max_length]
|
||||
|
||||
return pad(key, 32)
|
||||
while len(key) % 16 != 0:
|
||||
key += b'\0'
|
||||
return key
|
||||
|
||||
|
||||
class BaseCrypto:
|
||||
|
||||
def encrypt(self, text):
|
||||
return base64.urlsafe_b64encode(
|
||||
self._encrypt(bytes(text, encoding='utf8'))
|
||||
@@ -45,7 +52,7 @@ class BaseCrypto:
|
||||
|
||||
class GMSM4EcbCrypto(BaseCrypto):
|
||||
def __init__(self, key):
|
||||
self.key = process_key(key)
|
||||
self.key = padding_key(key, 16)
|
||||
self.sm4_encryptor = CryptSM4()
|
||||
self.sm4_encryptor.set_key(self.key, SM4_ENCRYPT)
|
||||
|
||||
@@ -59,6 +66,26 @@ class GMSM4EcbCrypto(BaseCrypto):
|
||||
return self.sm4_decryptor.crypt_ecb(data)
|
||||
|
||||
|
||||
class PiicoSM4EcbCrypto(BaseCrypto):
|
||||
|
||||
@staticmethod
|
||||
def to_16(key):
|
||||
while len(key) % 16 != 0:
|
||||
key += b'\0'
|
||||
return key # 返回bytes
|
||||
|
||||
def __init__(self, key, device: piico.Device):
|
||||
key = padding_key(key, 16)
|
||||
self.cipher = device.new_sm4_ebc_cipher(key)
|
||||
|
||||
def _encrypt(self, data: bytes) -> bytes:
|
||||
return self.cipher.encrypt(self.to_16(data))
|
||||
|
||||
def _decrypt(self, data: bytes) -> bytes:
|
||||
bs = self.cipher.decrypt(data)
|
||||
return bs.rstrip(b'\0')
|
||||
|
||||
|
||||
class AESCrypto:
|
||||
"""
|
||||
AES
|
||||
@@ -70,9 +97,8 @@ class AESCrypto:
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
if len(key) > 32:
|
||||
key = key[:32]
|
||||
self.key = self.to_16(key)
|
||||
self.key = padding_key(key, 32)
|
||||
self.aes = AES.new(self.key, AES.MODE_ECB)
|
||||
|
||||
@staticmethod
|
||||
def to_16(key):
|
||||
@@ -87,17 +113,15 @@ class AESCrypto:
|
||||
return key # 返回bytes
|
||||
|
||||
def aes(self):
|
||||
return AES.new(self.key, AES.MODE_ECB) # 初始化加密器
|
||||
return AES.new(self.key, AES.MODE_ECB)
|
||||
|
||||
def encrypt(self, text):
|
||||
aes = self.aes()
|
||||
cipher = base64.encodebytes(aes.encrypt(self.to_16(text)))
|
||||
cipher = base64.encodebytes(self.aes.encrypt(self.to_16(text)))
|
||||
return str(cipher, encoding='utf8').replace('\n', '') # 加密
|
||||
|
||||
def decrypt(self, text):
|
||||
aes = self.aes()
|
||||
text_decoded = base64.decodebytes(bytes(text, encoding='utf8'))
|
||||
return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
|
||||
return str(self.aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
|
||||
|
||||
|
||||
class AESCryptoGCM:
|
||||
@@ -106,7 +130,15 @@ class AESCryptoGCM:
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = process_key(key)
|
||||
self.key = self.process_key(key)
|
||||
|
||||
@staticmethod
|
||||
def process_key(key):
|
||||
if not isinstance(key, bytes):
|
||||
key = bytes(key, encoding='utf-8')
|
||||
if len(key) >= 32:
|
||||
return key[:32]
|
||||
return pad(key, 32)
|
||||
|
||||
def encrypt(self, text):
|
||||
"""
|
||||
@@ -133,7 +165,6 @@ class AESCryptoGCM:
|
||||
nonce = base64.b64decode(metadata[24:48])
|
||||
tag = base64.b64decode(metadata[48:])
|
||||
ciphertext = base64.b64decode(text[72:])
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
|
||||
|
||||
cipher.update(header)
|
||||
@@ -144,11 +175,10 @@ class AESCryptoGCM:
|
||||
def get_aes_crypto(key=None, mode='GCM'):
|
||||
if key is None:
|
||||
key = settings.SECRET_KEY
|
||||
if mode == 'ECB':
|
||||
a = AESCrypto(key)
|
||||
elif mode == 'GCM':
|
||||
a = AESCryptoGCM(key)
|
||||
return a
|
||||
if mode == 'GCM':
|
||||
return AESCryptoGCM(key)
|
||||
else:
|
||||
return AESCrypto(key)
|
||||
|
||||
|
||||
def get_gm_sm4_ecb_crypto(key=None):
|
||||
@@ -156,44 +186,65 @@ def get_gm_sm4_ecb_crypto(key=None):
|
||||
return GMSM4EcbCrypto(key)
|
||||
|
||||
|
||||
def get_piico_gm_sm4_ecb_crypto(device, key=None):
|
||||
key = key or settings.SECRET_KEY
|
||||
return PiicoSM4EcbCrypto(key, device)
|
||||
|
||||
|
||||
aes_ecb_crypto = get_aes_crypto(mode='ECB')
|
||||
aes_crypto = get_aes_crypto(mode='GCM')
|
||||
gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto()
|
||||
|
||||
|
||||
class Crypto:
|
||||
cryptoes = {
|
||||
cryptor_map = {
|
||||
'aes_ecb': aes_ecb_crypto,
|
||||
'aes_gcm': aes_crypto,
|
||||
'aes': aes_crypto,
|
||||
'gm_sm4_ecb': gm_sm4_ecb_crypto,
|
||||
'gm': gm_sm4_ecb_crypto,
|
||||
}
|
||||
cryptos = []
|
||||
|
||||
def __init__(self):
|
||||
cryptoes = self.__class__.cryptoes.copy()
|
||||
crypto = cryptoes.pop(settings.SECURITY_DATA_CRYPTO_ALGO, None)
|
||||
if crypto is None:
|
||||
crypt_algo = settings.SECURITY_DATA_CRYPTO_ALGO
|
||||
if not crypt_algo:
|
||||
if settings.GMSSL_ENABLED:
|
||||
if settings.PIICO_DEVICE_ENABLE:
|
||||
piico_driver_path = settings.PIICO_DRIVER_PATH if settings.PIICO_DRIVER_PATH \
|
||||
else "./lib/libpiico_ccmu.so"
|
||||
device = piico.open_piico_device(piico_driver_path)
|
||||
self.cryptor_map["piico_gm"] = get_piico_gm_sm4_ecb_crypto(device)
|
||||
crypt_algo = 'piico_gm'
|
||||
else:
|
||||
crypt_algo = 'gm'
|
||||
else:
|
||||
crypt_algo = 'aes'
|
||||
cryptor = self.cryptor_map.get(crypt_algo, None)
|
||||
if cryptor is None:
|
||||
raise ImproperlyConfigured(
|
||||
f'Crypto method not supported {settings.SECURITY_DATA_CRYPTO_ALGO}'
|
||||
)
|
||||
self.cryptoes = [crypto, *cryptoes.values()]
|
||||
others = set(self.cryptor_map.values()) - {cryptor}
|
||||
self.cryptos = [cryptor, *others]
|
||||
|
||||
@property
|
||||
def encryptor(self):
|
||||
return self.cryptoes[0]
|
||||
return self.cryptos[0]
|
||||
|
||||
def encrypt(self, text):
|
||||
if text is None:
|
||||
return text
|
||||
return self.encryptor.encrypt(text)
|
||||
|
||||
def decrypt(self, text):
|
||||
for decryptor in self.cryptoes:
|
||||
for cryptor in self.cryptos:
|
||||
try:
|
||||
origin_text = decryptor.decrypt(text)
|
||||
origin_text = cryptor.decrypt(text)
|
||||
if origin_text:
|
||||
# 有时不同算法解密不报错,但是返回空字符串
|
||||
return origin_text
|
||||
except (TypeError, ValueError, UnicodeDecodeError, IndexError):
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
@@ -255,6 +306,8 @@ def decrypt_password(value):
|
||||
if len(cipher) != 2:
|
||||
return value
|
||||
key_cipher, password_cipher = cipher
|
||||
if not all([key_cipher, password_cipher]):
|
||||
return value
|
||||
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
|
||||
aes = get_aes_crypto(aes_key, 'ECB')
|
||||
try:
|
||||
|
||||
@@ -196,7 +196,8 @@ def encrypt_password(password, salt=None, algorithm='sha512'):
|
||||
return des_crypt.hash(password, salt=salt[:2])
|
||||
|
||||
support_algorithm = {
|
||||
'sha512': sha512, 'des': des
|
||||
'sha512': sha512,
|
||||
'des': des
|
||||
}
|
||||
|
||||
if isinstance(algorithm, str):
|
||||
@@ -222,9 +223,6 @@ def ensure_last_char_is_ascii(data):
|
||||
remain = ''
|
||||
|
||||
|
||||
secret_pattern = re.compile(r'password|secret|key', re.IGNORECASE)
|
||||
|
||||
|
||||
def data_to_json(data, sort_keys=True, indent=2, cls=None):
|
||||
if cls is None:
|
||||
cls = DjangoJSONEncoder
|
||||
|
||||
@@ -4,6 +4,10 @@ import csv
|
||||
import pyzipper
|
||||
import requests
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def create_csv_file(filename, headers, rows, ):
|
||||
with open(filename, 'w', encoding='utf-8-sig')as f:
|
||||
@@ -28,3 +32,18 @@ def download_file(src, path):
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def save_content_to_temp_path(content, file_mode=0o400):
|
||||
if not content:
|
||||
return
|
||||
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
filename = '.' + md5(content.encode('utf-8')).hexdigest()
|
||||
filepath = os.path.join(tmp_dir, filename)
|
||||
if not os.path.exists(filepath):
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
os.chmod(filepath, file_mode)
|
||||
return filepath
|
||||
|
||||
@@ -35,7 +35,7 @@ def random_string(length, lower=True, upper=True, digit=True, special_char=False
|
||||
|
||||
if special_char:
|
||||
spc = random.choice(string_punctuation)
|
||||
i = random.choice(range(len(password)))
|
||||
i = random.choice(range(1, len(password)))
|
||||
password[i] = spc
|
||||
|
||||
password = ''.join(password)
|
||||
|
||||
@@ -15,18 +15,23 @@ import errno
|
||||
import json
|
||||
import yaml
|
||||
import copy
|
||||
import base64
|
||||
import logging
|
||||
from importlib import import_module
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||
XPACK_DIR = os.path.join(BASE_DIR, 'xpack')
|
||||
HAS_XPACK = os.path.isdir(XPACK_DIR)
|
||||
|
||||
logger = logging.getLogger('jumpserver.conf')
|
||||
|
||||
|
||||
def import_string(dotted_path):
|
||||
try:
|
||||
@@ -39,9 +44,9 @@ def import_string(dotted_path):
|
||||
try:
|
||||
return getattr(module, class_name)
|
||||
except AttributeError as err:
|
||||
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
|
||||
module_path, class_name)
|
||||
) from err
|
||||
raise ImportError(
|
||||
'Module "%s" does not define a "%s" attribute/class' %
|
||||
(module_path, class_name)) from err
|
||||
|
||||
|
||||
def is_absolute_uri(uri):
|
||||
@@ -80,6 +85,59 @@ class DoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigCrypto:
|
||||
secret_keys = [
|
||||
'SECRET_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD',
|
||||
]
|
||||
|
||||
def __init__(self, key):
|
||||
self.safe_key = self.process_key(key)
|
||||
self.sm4_encryptor = CryptSM4()
|
||||
self.sm4_encryptor.set_key(self.safe_key, SM4_ENCRYPT)
|
||||
|
||||
self.sm4_decryptor = CryptSM4()
|
||||
self.sm4_decryptor.set_key(self.safe_key, SM4_DECRYPT)
|
||||
|
||||
@staticmethod
|
||||
def process_key(secret_encrypt_key):
|
||||
key = secret_encrypt_key.encode()
|
||||
if len(key) >= 16:
|
||||
key = key[:16]
|
||||
else:
|
||||
key += b'\0' * (16 - len(key))
|
||||
return key
|
||||
|
||||
def encrypt(self, data):
|
||||
data = bytes(data, encoding='utf8')
|
||||
return base64.b64encode(self.sm4_encryptor.crypt_ecb(data)).decode('utf8')
|
||||
|
||||
def decrypt(self, data):
|
||||
data = base64.urlsafe_b64decode(bytes(data, encoding='utf8'))
|
||||
return self.sm4_decryptor.crypt_ecb(data).decode('utf8')
|
||||
|
||||
def decrypt_if_need(self, value, item):
|
||||
if item not in self.secret_keys:
|
||||
return value
|
||||
|
||||
try:
|
||||
plaintext = self.decrypt(value)
|
||||
if plaintext:
|
||||
value = plaintext
|
||||
except Exception as e:
|
||||
logger.error('decrypt %s error: %s', item, e)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_secret_encryptor(cls):
|
||||
# 使用 SM4 加密配置文件敏感信息
|
||||
# https://the-x.cn/cryptography/Sm4.aspx
|
||||
secret_encrypt_key = os.environ.get('SECRET_ENCRYPT_KEY', '')
|
||||
if not secret_encrypt_key:
|
||||
return None
|
||||
print('Info: Using SM4 to encrypt config secret value')
|
||||
return cls(secret_encrypt_key)
|
||||
|
||||
|
||||
class Config(dict):
|
||||
"""Works exactly like a dict but provides ways to fill it from files
|
||||
or special dictionaries. There are two common patterns to populate the
|
||||
@@ -160,12 +218,15 @@ class Config(dict):
|
||||
'SESSION_COOKIE_DOMAIN': None,
|
||||
'CSRF_COOKIE_DOMAIN': None,
|
||||
'SESSION_COOKIE_NAME_PREFIX': None,
|
||||
'SESSION_COOKIE_AGE': 3600,
|
||||
'SESSION_COOKIE_AGE': 3600 * 24,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
|
||||
'LOGIN_URL': reverse_lazy('authentication:login'),
|
||||
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
|
||||
|
||||
# Custom Config
|
||||
'AUTH_CUSTOM': False,
|
||||
'AUTH_CUSTOM_FILE_MD5': '',
|
||||
|
||||
# Auth LDAP settings
|
||||
'AUTH_LDAP': False,
|
||||
'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389',
|
||||
@@ -265,6 +326,24 @@ class Config(dict):
|
||||
'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
|
||||
'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
|
||||
|
||||
# OAuth2 认证
|
||||
'AUTH_OAUTH2': False,
|
||||
'AUTH_OAUTH2_LOGO_PATH': 'img/login_oauth2_logo.png',
|
||||
'AUTH_OAUTH2_PROVIDER': 'OAuth2',
|
||||
'AUTH_OAUTH2_ALWAYS_UPDATE_USER': True,
|
||||
'AUTH_OAUTH2_CLIENT_ID': 'client-id',
|
||||
'AUTH_OAUTH2_SCOPE': '',
|
||||
'AUTH_OAUTH2_CLIENT_SECRET': '',
|
||||
'AUTH_OAUTH2_LOGOUT_COMPLETELY': True,
|
||||
'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize',
|
||||
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo',
|
||||
'AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT': 'https://oauth2.example.com/logout',
|
||||
'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT': 'https://oauth2.example.com/access_token',
|
||||
'AUTH_OAUTH2_ACCESS_TOKEN_METHOD': 'GET',
|
||||
'AUTH_OAUTH2_USER_ATTR_MAP': {
|
||||
'name': 'name', 'username': 'username', 'email': 'email'
|
||||
},
|
||||
|
||||
'AUTH_TEMP_TOKEN': False,
|
||||
|
||||
# 企业微信
|
||||
@@ -302,6 +381,22 @@ class Config(dict):
|
||||
'TENCENT_VERIFY_SIGN_NAME': '',
|
||||
'TENCENT_VERIFY_TEMPLATE_CODE': '',
|
||||
|
||||
'HUAWEI_APP_KEY': '',
|
||||
'HUAWEI_APP_SECRET': '',
|
||||
'HUAWEI_SMS_ENDPOINT': '',
|
||||
'HUAWEI_SIGN_CHANNEL_NUM': '',
|
||||
'HUAWEI_VERIFY_SIGN_NAME': '',
|
||||
'HUAWEI_VERIFY_TEMPLATE_CODE': '',
|
||||
|
||||
'CMPP2_HOST': '',
|
||||
'CMPP2_PORT': 7890,
|
||||
'CMPP2_SP_ID': '',
|
||||
'CMPP2_SP_SECRET': '',
|
||||
'CMPP2_SRC_ID': '',
|
||||
'CMPP2_SERVICE_ID': '',
|
||||
'CMPP2_VERIFY_SIGN_NAME': '',
|
||||
'CMPP2_VERIFY_TEMPLATE_CODE': '{code}',
|
||||
|
||||
# Email
|
||||
'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'),
|
||||
'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'),
|
||||
@@ -387,7 +482,10 @@ class Config(dict):
|
||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||
'SERVER_REPLAY_STORAGE': {},
|
||||
'SECURITY_DATA_CRYPTO_ALGO': 'aes',
|
||||
'SECURITY_DATA_CRYPTO_ALGO': None,
|
||||
'GMSSL_ENABLED': False,
|
||||
# Magnus 组件需要监听的端口范围
|
||||
'MAGNUS_PORTS': '30000-30100',
|
||||
|
||||
# 记录清理清理
|
||||
'LOGIN_LOG_KEEP_DAYS': 200,
|
||||
@@ -405,6 +503,7 @@ class Config(dict):
|
||||
'CONNECTION_TOKEN_ENABLED': False,
|
||||
|
||||
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
|
||||
'TICKET_AUTHORIZE_DEFAULT_TIME': 7,
|
||||
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
|
||||
'PERIOD_TASK_ENABLED': True,
|
||||
|
||||
@@ -416,6 +515,10 @@ class Config(dict):
|
||||
'HEALTH_CHECK_TOKEN': '',
|
||||
}
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.secret_encryptor = ConfigCrypto.get_secret_encryptor()
|
||||
|
||||
@staticmethod
|
||||
def convert_keycloak_to_openid(keycloak_config):
|
||||
"""
|
||||
@@ -427,7 +530,6 @@ class Config(dict):
|
||||
"""
|
||||
|
||||
openid_config = copy.deepcopy(keycloak_config)
|
||||
|
||||
auth_openid = openid_config.get('AUTH_OPENID')
|
||||
auth_openid_realm_name = openid_config.get('AUTH_OPENID_REALM_NAME')
|
||||
auth_openid_server_url = openid_config.get('AUTH_OPENID_SERVER_URL')
|
||||
@@ -556,13 +658,12 @@ class Config(dict):
|
||||
def get(self, item):
|
||||
# 再从配置文件中获取
|
||||
value = self.get_from_config(item)
|
||||
if value is not None:
|
||||
return value
|
||||
# 其次从环境变量来
|
||||
value = self.get_from_env(item)
|
||||
if value is not None:
|
||||
return value
|
||||
value = self.defaults.get(item)
|
||||
if value is None:
|
||||
value = self.get_from_env(item)
|
||||
if value is None:
|
||||
value = self.defaults.get(item)
|
||||
if self.secret_encryptor:
|
||||
value = self.secret_encryptor.decrypt_if_need(value, item)
|
||||
return value
|
||||
|
||||
def __getitem__(self, item):
|
||||
|
||||
@@ -4,10 +4,10 @@ from django.core.asgi import get_asgi_application
|
||||
|
||||
from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
|
||||
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
|
||||
from settings.urls.ws_urls import urlpatterns as setting_urlpatterns
|
||||
|
||||
urlpatterns = []
|
||||
urlpatterns += ops_urlpatterns \
|
||||
+ notifications_urlpatterns
|
||||
urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
'websocket': AuthMiddlewareStack(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#
|
||||
import os
|
||||
import ldap
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..const import CONFIG, PROJECT_DIR, BASE_DIR
|
||||
|
||||
@@ -24,9 +23,15 @@ AUTH_LDAP_GLOBAL_OPTIONS = {
|
||||
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
|
||||
ldap.OPT_REFERRALS: CONFIG.AUTH_LDAP_OPTIONS_OPT_REFERRALS
|
||||
}
|
||||
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
|
||||
LDAP_CACERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
|
||||
if os.path.isfile(LDAP_CACERT_FILE):
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CACERT_FILE
|
||||
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.pem")
|
||||
if os.path.isfile(LDAP_CERT_FILE):
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_FILE
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CERTFILE] = LDAP_CERT_FILE
|
||||
LDAP_KEY_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.key")
|
||||
if os.path.isfile(LDAP_KEY_FILE):
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_KEYFILE] = LDAP_KEY_FILE
|
||||
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
|
||||
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
|
||||
# AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||
@@ -143,6 +148,26 @@ SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS
|
||||
SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
|
||||
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
|
||||
|
||||
# OAuth2 auth
|
||||
AUTH_OAUTH2 = CONFIG.AUTH_OAUTH2
|
||||
AUTH_OAUTH2_LOGO_PATH = CONFIG.AUTH_OAUTH2_LOGO_PATH
|
||||
AUTH_OAUTH2_PROVIDER = CONFIG.AUTH_OAUTH2_PROVIDER
|
||||
AUTH_OAUTH2_ALWAYS_UPDATE_USER = CONFIG.AUTH_OAUTH2_ALWAYS_UPDATE_USER
|
||||
AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_METHOD = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_METHOD
|
||||
AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
|
||||
AUTH_OAUTH2_CLIENT_SECRET = CONFIG.AUTH_OAUTH2_CLIENT_SECRET
|
||||
AUTH_OAUTH2_CLIENT_ID = CONFIG.AUTH_OAUTH2_CLIENT_ID
|
||||
AUTH_OAUTH2_SCOPE = CONFIG.AUTH_OAUTH2_SCOPE
|
||||
AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP
|
||||
AUTH_OAUTH2_LOGOUT_COMPLETELY = CONFIG.AUTH_OAUTH2_LOGOUT_COMPLETELY
|
||||
AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
|
||||
AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback'
|
||||
AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/'
|
||||
AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/'
|
||||
AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout"
|
||||
|
||||
# 临时 token
|
||||
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
||||
|
||||
@@ -170,8 +195,9 @@ AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
|
||||
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
|
||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
|
||||
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
|
||||
AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend'
|
||||
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
|
||||
|
||||
AUTH_BACKEND_CUSTOM = 'authentication.backends.custom.CustomAuthBackend'
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
# 只做权限校验
|
||||
@@ -180,12 +206,37 @@ AUTHENTICATION_BACKENDS = [
|
||||
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS,
|
||||
# 跳转形式
|
||||
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
|
||||
AUTH_BACKEND_OAUTH2,
|
||||
# 扫码模式
|
||||
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
|
||||
# Token模式
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN,
|
||||
]
|
||||
|
||||
|
||||
def get_file_md5(filepath):
|
||||
import hashlib
|
||||
# 创建md5对象
|
||||
m = hashlib.md5()
|
||||
with open(filepath, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(4096)
|
||||
if not data:
|
||||
break
|
||||
# 更新md5对象
|
||||
m.update(data)
|
||||
# 返回md5对象
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
AUTH_CUSTOM = CONFIG.AUTH_CUSTOM
|
||||
AUTH_CUSTOM_FILE_MD5 = CONFIG.AUTH_CUSTOM_FILE_MD5
|
||||
AUTH_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'auth', 'main.py')
|
||||
if AUTH_CUSTOM and AUTH_CUSTOM_FILE_MD5 == get_file_md5(AUTH_CUSTOM_FILE_PATH):
|
||||
# 自定义认证模块
|
||||
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_CUSTOM)
|
||||
|
||||
AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2]
|
||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV
|
||||
# Absolute url for some case, for example email link
|
||||
SITE_URL = CONFIG.SITE_URL
|
||||
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# LOG LEVEL
|
||||
LOG_LEVEL = CONFIG.LOG_LEVEL
|
||||
|
||||
@@ -106,6 +109,7 @@ MIDDLEWARE = [
|
||||
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
|
||||
'authentication.backends.cas.middleware.CASMiddleware',
|
||||
'authentication.middleware.MFAMiddleware',
|
||||
'authentication.middleware.ThirdPartyLoginMiddleware',
|
||||
'authentication.middleware.SessionCookieMiddleware',
|
||||
'simple_history.middleware.HistoryRequestMiddleware',
|
||||
]
|
||||
@@ -138,6 +142,7 @@ WSGI_APPLICATION = 'jumpserver.wsgi.application'
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
||||
LOGIN_URL = reverse_lazy('authentication:login')
|
||||
LOGOUT_REDIRECT_URL = CONFIG.LOGOUT_REDIRECT_URL
|
||||
|
||||
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
CSRF_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
@@ -307,6 +312,21 @@ CSRF_COOKIE_SECURE = CONFIG.CSRF_COOKIE_SECURE
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
]
|
||||
|
||||
|
||||
GMSSL_ENABLED = CONFIG.GMSSL_ENABLED
|
||||
GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher'
|
||||
if GMSSL_ENABLED:
|
||||
PASSWORD_HASHERS.insert(0, GM_HASHER)
|
||||
else:
|
||||
PASSWORD_HASHERS.append(GM_HASHER)
|
||||
|
||||
# For Debug toolbar
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
if os.environ.get('DEBUG_TOOLBAR', False):
|
||||
@@ -315,3 +335,4 @@ if os.environ.get('DEBUG_TOOLBAR', False):
|
||||
DEBUG_TOOLBAR_PANELS = [
|
||||
'debug_toolbar.panels.profiling.ProfilingPanel',
|
||||
]
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
|
||||
BACKEND_ASSET_USER_AUTH_VAULT = False
|
||||
|
||||
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
|
||||
TICKET_AUTHORIZE_DEFAULT_TIME = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME
|
||||
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
|
||||
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
|
||||
FLOWER_URL = CONFIG.FLOWER_URL
|
||||
@@ -110,6 +111,8 @@ HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
|
||||
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
|
||||
LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS
|
||||
TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS
|
||||
OPERATE_LOG_KEEP_DAYS = CONFIG.OPERATE_LOG_KEEP_DAYS
|
||||
FTP_LOG_KEEP_DAYS = CONFIG.FTP_LOG_KEEP_DAYS
|
||||
ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL
|
||||
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD
|
||||
|
||||
@@ -174,3 +177,6 @@ HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
|
||||
|
||||
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
|
||||
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
|
||||
|
||||
# Magnus DB Port
|
||||
MAGNUS_PORTS = CONFIG.MAGNUS_PORTS
|
||||
|
||||
@@ -9,7 +9,6 @@ from .base import (
|
||||
)
|
||||
from ..const import CONFIG, PROJECT_DIR
|
||||
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
# or allow read-only access for unauthenticated users.
|
||||
@@ -64,7 +63,6 @@ SWAGGER_SETTINGS = {
|
||||
'DEFAULT_INFO': 'jumpserver.views.swagger.api_info',
|
||||
}
|
||||
|
||||
|
||||
# Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html
|
||||
CAPTCHA_IMAGE_SIZE = (180, 38)
|
||||
CAPTCHA_FOREGROUND_COLOR = '#001100'
|
||||
@@ -81,7 +79,6 @@ BOOTSTRAP3 = {
|
||||
'required_css_class': 'required',
|
||||
}
|
||||
|
||||
|
||||
# Django channels support websocket
|
||||
if not REDIS_USE_SSL:
|
||||
redis_ssl = None
|
||||
@@ -101,14 +98,13 @@ CHANNEL_LAYERS = {
|
||||
'address': (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT),
|
||||
'db': CONFIG.REDIS_DB_WS,
|
||||
'password': CONFIG.REDIS_PASSWORD or None,
|
||||
'ssl': redis_ssl
|
||||
'ssl': redis_ssl
|
||||
}],
|
||||
},
|
||||
},
|
||||
}
|
||||
ASGI_APPLICATION = 'jumpserver.routing.application'
|
||||
|
||||
|
||||
# Dump all celery log to here
|
||||
CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery')
|
||||
|
||||
@@ -131,6 +127,7 @@ CELERY_TASK_EAGER_PROPAGATES = True
|
||||
CELERY_WORKER_REDIRECT_STDOUTS = True
|
||||
CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 3600
|
||||
CELERY_WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS = True
|
||||
|
||||
if REDIS_USE_SSL:
|
||||
CELERY_BROKER_USE_SSL = CELERY_REDIS_BACKEND_USE_SSL = {
|
||||
@@ -148,3 +145,7 @@ REDIS_PORT = CONFIG.REDIS_PORT
|
||||
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
|
||||
|
||||
DJANGO_REDIS_SCAN_ITERSIZE = 1000
|
||||
|
||||
# GM DEVICE
|
||||
PIICO_DEVICE_ENABLE = CONFIG.PIICO_DEVICE_ENABLE
|
||||
PIICO_DRIVER_PATH = CONFIG.PIICO_DRIVER_PATH
|
||||
|
||||
@@ -154,4 +154,4 @@ if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2:
|
||||
})
|
||||
|
||||
if not os.path.isdir(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
os.makedirs(LOG_DIR, mode=0o755)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f2fdd3a7bd34a26d068fc6ce521d0ea9983c477b13536ba3f51700a554d4ae3
|
||||
size 128706
|
||||
oid sha256:7522cd9a7e7853d078c81006cea7f6dbe4fb9d51ae7c6dddd50e8471536d4c0d
|
||||
size 133026
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user