Compare commits

...

131 Commits

Author SHA1 Message Date
fit2bot
a497b3cf94 fix: Add '/media/' to the list of whitelisted URLs for MFA login (#16412)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-10 14:19:39 +08:00
fit2bot
8548b73063 fix: Failed to switch languages (#16326)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-24 11:13:56 +08:00
fit2bot
182320f492 fix: SAML2 authentication failure with Okta integration (#16250)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-07 15:42:09 +08:00
fit2bot
40d326d6a6 fix: Any change to the LDAP server URI should require re-authentication and explicit re-entry of (#16195)
the bind password, not reuse stored credentials

Co-authored-by: wangruidong <940853815@qq.com>
2025-10-23 18:09:46 +08:00
ibuler
b87554f9db perf: conn token get 2025-10-21 11:05:05 +08:00
Bai
3c255f9fa6 fix: AK/SK remained valid after the user expired. 2025-09-16 13:31:53 +08:00
fit2bot
a6b5437f6a fix: Open redirect security vulnerability (#15938)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-27 11:03:30 +08:00
feng
71c690ef9e perf: Account remove 2025-08-26 17:28:28 +08:00
feng
bee07db900 perf: Windows change secret check_conn_after_change 2025-08-26 14:55:01 +08:00
Bryan
115eb7c15a Reformat import statements in utils.py 2025-08-25 18:20:18 +08:00
Bai
88810263cd perf: test rebase for commit id changed 2 2025-08-25 18:16:15 +08:00
Bai
3d95bc4656 perf: test rebase for commit id changed 2025-08-25 18:05:47 +08:00
feng
69de08bb5d fix: Ticket filtering current reviewer issue 2025-08-25 14:35:15 +08:00
Bai
5c234fdd0c perf: lock xmlsec==1.3.13 2025-08-25 11:19:24 +08:00
fit2bot
be5baa5a3f perf: Update IP group validation to include address validation (#15919)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-25 11:16:52 +08:00
feng
2f1a65f120 perf: migrate 2025-08-25 11:12:25 +08:00
fit2bot
e6d02eaf4c fix: Add third party login check is block (#15916)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-25 10:51:42 +08:00
fit2bot
6d6dec2752 fix: Prevent nested resource issues in type nodes tree API (#15915)
Co-authored-by: wangruidong <940853815@qq.com>
2025-08-25 10:40:10 +08:00
feng
c6c067c44b perf: Mongodb ping 2025-08-19 19:09:52 +08:00
feng
84ec1b047a perf: FormatAssetInfo posix_format cpu_model 2025-08-15 16:51:33 +08:00
feng
e6dca2ec14 fix: automation mysql priv and postgresql finally test the connectivity 2025-08-13 15:34:32 +08:00
wangruidong
8793003d18 perf: check_asset_permission_will_expired filter is_active=True 2025-08-07 10:18:24 +08:00
ibuler
29fd6e0ae4 perf: update pymyssql 2025-07-24 19:17:01 +08:00
Ewall555
90587a83cc feat: support rbac SSO token 2025-07-16 19:21:53 +08:00
ibuler
dfa0198742 perf: safe db connection on runner success 2025-07-11 11:00:40 +08:00
jiangweidong
9e857b54ed fix: According to the CMPP2.0 protocol standard, modify the attribute alignment. 2025-06-26 18:41:59 +08:00
Ewall555
c34358509b perf: Update metismenu plugin to version 3.0.7 2025-06-24 10:24:05 +08:00
feng
a7c46109d9 fix: Fix the problem of changing the password and retrying to obtain the password 2025-06-17 18:13:29 +08:00
feng
48fa6172bd perf: Suggestion api 2025-06-16 14:17:45 +08:00
wangruidong
89aa87fd6b fix: Failed to update database assets 2025-06-10 18:05:17 +08:00
ewall555
79d230755e perf: Update jsencrypt library version 2025-06-09 18:42:44 +08:00
feng
99082f261e perf: Optimize the results returned by the suggestion api for different organizations 2025-06-06 18:08:31 +08:00
wangruidong
7e2100b435 perf: set ansible_timeout for account connectivity tasks 2025-06-04 18:41:14 +08:00
wangruidong
185d4e9563 fix: Ensure platform_id is a digit before querying Platform 2025-05-29 16:18:47 +08:00
feng
ecaa84790c perf: SSO add mfa 2025-05-20 13:11:38 +08:00
Chenyang Shen
30210dc0b9 Merge pull request #15423 from jumpserver/pr@v3@feat_add_new_alg
feat: add a new piico gm alg
2025-05-15 17:44:34 +08:00
Aaron3S
ff699f4ee2 feat: add a new piico gm alg 2025-05-15 09:43:23 +00:00
ewall555
48239b0c63 feat: Set the default expiration days for adding user and asset permissions 2025-05-13 10:57:23 +08:00
ibuler
f4f74909a8 fix: update session error when db is pg 2025-04-21 13:30:09 +08:00
feng
cab1e0bf52 perf: Perm the template push account 2025-03-27 13:59:13 +08:00
feng
bf195c1599 fix: check_api 2025-03-27 13:00:14 +08:00
feng
7f5f7e81b8 perf: change secret change_secret_result 2025-03-26 23:28:22 +08:00
feng
99affad9b9 perf: Add check_conn_after_change 2025-03-26 18:01:26 +08:00
halo
34eea024f8 perf: Use a domain account to avoid automatically creating a local account 2025-03-25 14:18:04 +08:00
Bai
1d1e4b90ed perf: update pyproject.toml 2025-03-24 15:20:27 +08:00
feng
f5d40a787e fix: check_api 2025-03-24 14:34:44 +08:00
jiangweidong
77d8083c00 fix: Slove the problem that the third-party auth cannot update user name 2025-03-06 17:03:19 +08:00
fit2bot
180303ccb4 fix: Import failed but no prompt message (#14966)
* fix: Import failed but no prompt message

* fix: Prompt message

---------

Co-authored-by: halo <wuyihuangw@gmail.com>
2025-03-04 14:44:57 +08:00
jiangweidong
9cd1619990 fix: Solve the problem that some messages cannot be sent from unauthenticated email 2025-02-28 17:45:02 +08:00
wangruidong
7d0a901522 fix: When the organization does not exist, close ticket with an error. 2025-02-13 17:51:40 +08:00
wangruidong
5e9fabff1b fix: markdown render issue 2025-02-12 15:46:54 +08:00
wangruidong
1d36934111 fix: Cannot set original org when exception occurs 2025-02-08 11:17:24 +08:00
Bai
25603e4758 fix: setting field encrypt issue 2025-02-06 17:11:43 +08:00
Bai
3ae164d7e0 fix: circle imported for perms-api 2025-01-08 10:34:46 +08:00
wangruidong
3ad64e142e fix: circular import 2025-01-07 14:08:49 +08:00
wangruidong
0ff1413780 perf: ticket info add org name 2025-01-06 14:09:49 +08:00
jiangweidong
f5b64bed4e feat: VMware 自动同步文件夹到节点-翻译 2025-01-03 18:50:30 +08:00
Bai
a559415b65 fix: koko press r dont refresh user perm-nodes 2025-01-03 17:13:49 +08:00
Bai
2e7bd076f4 fix: limit connect method xpack 2024-12-25 16:27:51 +08:00
Bai
11f6fe0bf9 fix: system org 2024-12-25 15:34:19 +08:00
wangruidong
ae94648e80 fix: Add type check for secure command execution 2024-12-24 15:58:15 +08:00
jiangweidong
94e08f3d96 perf: The command amount does not record operation logs 2024-12-20 14:59:53 +08:00
Bai
8bedef92f0 fix: api prometheus count 2024-12-20 10:57:42 +08:00
jiangweidong
e5bb28231a perf: Oauth2.0 support two methods for passing authentication credentials. 2024-12-19 14:27:29 +08:00
jiangweidong
b5aeb24ae9 perf: create account add activity log 2024-12-18 15:53:08 +08:00
feng
674ea7142f perf: The entire organization can view activity log 2024-12-11 16:21:39 +08:00
fit2bot
5ab7b99b9d perf: add encrypted configuration API (#14633)
* perf: 添加加密配置API

* perf: modify url

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 11:42:34 +08:00
Bai
9cd163c99d fix: when oidc enabled and use_state user login raise 400 2024-12-06 16:26:59 +08:00
wangruidong
e72073f0cc perf: Add viewAssetOnlineSessionInfo conf 2024-11-25 15:26:14 +08:00
wangruidong
690f525afc perf: Add check for SECURITY_COMMAND_EXECUTION settings in ops tasks 2024-11-11 18:15:11 +08:00
wangruidong
6686afcec1 fix: Password reset is only required for AUTH_BACKEND_MODEL 2024-10-23 11:04:15 +08:00
wangruidong
0918f5c6f6 perf: Translate 2024-10-22 17:49:22 +08:00
wangruidong
891e3d5609 perf: Storage update comment failed 2024-10-22 17:28:47 +08:00
wangruidong
9fad591545 fix: Historical sessions download failed 2024-10-22 16:34:46 +08:00
fit2bot
1ed1c3a536 perf: optimize the connection of operation logs to ES to prevent ES downtime from causing the core component to become unhealthy. (#14283)
* perf: optimize the connection of operation logs to ES to prevent ES downtime from causing the core component to become unhealthy.

* perf: sync publish message

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-10-12 16:18:18 +08:00
wangruidong
63824d3491 fix: adhoc execute alert msg 2024-10-12 16:15:48 +08:00
wangruidong
96eadf060c perf: site msg content optimize 2024-10-11 11:30:59 +08:00
Bai
2c9128b0e7 perf: DEFAULT_PAGE_SIZE same as MAX_LIMIT_PER_PAGE 2024-10-10 18:00:26 +08:00
Bai
7d6fd0f881 fix: Fixed the issue that the workbench user login log only displays failed logs 2024-09-29 14:45:59 +08:00
jiangweidong
4e996afd5e perf: Cloud Sync IP Policy Updated to Preferred Option i18n 2024-09-27 14:29:08 +08:00
feng
1ed745d042 perf: Login encryption key cache added 2024-09-26 15:11:56 +08:00
feng
39ebbfcf10 perf: The locked ip shows the username 2024-09-06 11:00:39 +08:00
wangruidong
d0ec4f798b perf: Optimize asset connection speed with es command storage 2024-08-30 10:53:03 +08:00
feng
1712a9a104 perf: No permission to test asset connectivity 2024-08-21 11:33:03 +08:00
Bryan
951aafcabd fix: v3 apps/authentication migrations won't be applied 2024-08-20 19:05:43 +08:00
wangruidong
e46da9d741 perf: Translate 2024-08-20 16:41:50 +08:00
ibuler
06aaf9e3d0 revert: dockerfile change 2024-08-20 14:32:50 +08:00
ibuler
5ac9fb81dc perf: change docker file 2024-08-20 13:59:49 +08:00
ibuler
fb907e250c perf: change docker file 2024-08-20 13:51:00 +08:00
ibuler
34ea40a14d perf: change docker file 2024-08-19 16:45:57 +08:00
ibuler
8c560b0317 perf: add dockerfile 2024-08-19 16:13:06 +08:00
wangruidong
df9cc4700b perf: Improve performance by optimizing ES index creation 2024-08-16 18:18:33 +08:00
wangruidong
6aa1227e60 fix: call get_verify_state_failed_response NotImplementedError 2024-08-15 20:18:34 +08:00
Bai
296c788e28 fix: job periodic task double run 2024-08-15 20:17:48 +08:00
fit2bot
a1ae29d35e fix: Use only_sudo failed (#13966)
* fix: Use only_sudo failed

* fix: Use only_sudo failed

* fix: Use only_sudo failed

---------

Co-authored-by: feng <1304903146@qq.com>
2024-08-14 16:15:07 +08:00
fit2bot
139ffd0b47 perf: Automation remove account task fail (#13406)
Co-authored-by: feng <1304903146@qq.com>
2024-08-12 18:26:18 +08:00
feng
ff6c1aef7f perf: Activity log no display 2024-08-08 16:39:13 +08:00
Eric
9b6b48a7f1 perf: support only su or sudo 2024-08-07 14:23:26 +08:00
wangruidong
2b133a8085 perf: object storage builtin comment i18n 2024-08-06 10:45:32 +08:00
Eric
81b5f1ce93 perf: Check whether the applet is available. 2024-08-05 18:18:05 +08:00
feng
c646084c51 perf: Ticket set serial number add lock 2024-08-05 17:53:08 +08:00
wangruidong
5e69c03cb7 perf: Remove applets, no longer display remote application connection methods 2024-08-01 15:59:35 +08:00
wangruidong
e2df85bddd fix: stop job failed 2024-07-30 18:49:50 +08:00
feng
d710697fa9 perf: Saml2 callback url miss port 2024-07-26 18:14:49 +08:00
halo
a955fcd682 perf: Email service authentication username is optional 2024-07-26 14:04:47 +08:00
Bai
1816d52d21 perf: Modifying the label matching logic of an AppletHost (random) 2024-07-25 19:02:41 +08:00
feng
d7c26cab7d fix: Console dashboard user login count 2024-07-24 16:10:05 +08:00
wangruidong
dc894fdc2d perf: Modify error message for desktop client login 2024-07-23 14:03:39 +08:00
feng
742ef89bef perf: You can modify sudo permissions multiple times 2024-07-22 17:27:04 +08:00
feng
3d4fc56592 perf: Gpt3 to gpt-4o-mini 2024-07-19 11:56:41 +08:00
feng
45291aba0c perf: The gateway password contains ! Password parsing failed 2024-07-19 10:41:41 +08:00
feng
495ee99e29 perf: Create authorization to add template account Push account parameters 2024-07-18 19:15:59 +08:00
jiangweidong
223eb8ad38 fix: async sms task params can json 2024-07-12 18:38:02 +08:00
gerry-fit
370e959400 perf: Enterprise Edition Hide Footer Copyright Content 2024-07-11 11:47:22 +08:00
fit2bot
b82f007787 perf: Migrate (#13689)
Co-authored-by: feng <1304903146@qq.com>
2024-07-11 10:35:01 +08:00
Eric
1faeb54673 perf: update locale i18n files 2024-07-10 18:42:24 +08:00
wangruidong
04e102cb87 fix: 定时清理任务不生效问题 2024-07-10 15:39:26 +08:00
fit2bot
81027cd561 perf: save_passwd_change filter user source local and passwords not emtpy (#13680)
Co-authored-by: feng <1304903146@qq.com>
2024-07-10 14:25:43 +08:00
fit2bot
cf727d22c0 fix: Account tempale cannot push params (#13671)
Co-authored-by: feng <1304903146@qq.com>
2024-07-09 19:12:24 +08:00
feng
bb6d077645 perf: save_passwd_change filter user source local and passwords not emtpy 2024-07-09 19:07:49 +08:00
halo
a78ccc9667 perf: 优化创建子节点时锁置后 2024-07-09 15:15:36 +08:00
Eric
d70351e6b3 perf: add i18n .mo file 2024-07-09 15:11:21 +08:00
Eric
4e76207adb perf: add i18n 2024-07-09 14:44:59 +08:00
ibuler
7a12c3737f perf: xpack can disable force 2024-07-09 11:10:03 +08:00
吴小白
8450c49e25 Merge pull request #13639 from jumpserver/pr@v3@update_poetry_lock
perf: update poetry lock
2024-07-09 10:59:00 +08:00
吴小白
ab6d8df2f0 perf: update poetry lock 2024-07-09 10:48:04 +08:00
Bai
550115c39f perf: update poetry lock 2024-07-08 19:42:43 +08:00
Eric
9c23512d91 perf: add connection options for mongodb 2024-07-08 18:21:57 +08:00
ibuler
30054b286a perf: change ansible version 2024-07-08 14:29:25 +08:00
Eric
22d7385891 perf: clean mp4 replay file 2024-06-25 19:07:17 +08:00
Bai
1701bedb41 perf: Update poetry lock file 2024-06-24 14:42:05 +08:00
182 changed files with 15363 additions and 4016 deletions

137
Dockerfile Normal file
View File

@@ -0,0 +1,137 @@
FROM python:3.11-slim-bullseye AS stage-1
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN echo > /opt/jumpserver/config.yml \
&& cd utils && bash -ixeu build.sh
FROM python:3.11-slim-bullseye as stage-2
ARG TARGETARCH
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
freetds-dev \
libffi-dev \
libjpeg-dev \
libkrb5-dev \
libldap2-dev \
libpq-dev \
libsasl2-dev \
libssl-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
freerdp2-dev \
libaio-dev"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
git \
git-lfs \
unzip \
xz-utils \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install
FROM python:3.11-slim-bullseye
ARG TARGETARCH
ENV LANG=zh_CN.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libjpeg-dev \
libpq-dev \
libx11-dev \
freerdp2-dev \
libxmlsec1-openssl"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
iputils-ping \
locales \
netcat-openbsd \
nmap \
openssh-client \
patch \
sshpass \
telnet \
vim \
bubblewrap \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc
ARG RECEPTOR_VERSION=v1.4.5
RUN set -ex \
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
&& chown root:root /usr/local/bin/receptor \
&& chmod 755 /usr/local/bin/receptor \
&& rm -f /opt/receptor.tar.gz
COPY --from=stage-2 /opt/py3 /opt/py3
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
COPY --from=stage-1 /opt/jumpserver/release/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
WORKDIR /opt/jumpserver
ARG VERSION
ENV VERSION=$VERSION
VOLUME /opt/jumpserver/data
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-bullseye as stage-1
FROM python:3.11-slim-bullseye AS stage-1
ARG TARGETARCH
ARG VERSION

View File

@@ -1,5 +1,5 @@
ARG VERSION
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack

View File

@@ -53,3 +53,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
register: result
failed_when: not result.is_available

View File

@@ -36,7 +36,8 @@
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true
when: db_info is succeeded

View File

@@ -39,3 +39,5 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
register: result
failed_when: not result.is_available

View File

@@ -35,12 +35,24 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
password: "{{ account.secret | password_hash('des') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -57,19 +69,9 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,5 +101,7 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

@@ -5,6 +5,12 @@ type:
- AIX
method: change_secret
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
en: 'Using Ansible module user to change account secret (DES)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -35,12 +35,24 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -57,19 +69,9 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,5 +101,7 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

@@ -6,6 +6,12 @@ type:
- linux
method: change_secret
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
en: 'Using Ansible module user to change account secret (SHA512)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -28,4 +28,6 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change

View File

@@ -31,5 +31,7 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change
delegate_to: localhost

View File

@@ -134,6 +134,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
record_id = self.record_map[asset_account_id]
try:
recorder = ChangeSecretRecord.objects.get(id=record_id)
new_secret = recorder.new_secret
except ChangeSecretRecord.DoesNotExist:
print(f"Record {record_id} not found")
continue

View File

@@ -53,3 +53,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
register: result
failed_when: not result.is_available

View File

@@ -36,7 +36,8 @@
name: "{{ account.username }}"
password: "{{ account.secret }}"
host: "%"
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true
when: db_info is succeeded

View File

@@ -31,7 +31,6 @@
role_attr_flags: LOGIN
ignore_errors: true
when: result is succeeded
register: change_info
- name: Verify password
community.postgresql.postgresql_ping:
@@ -40,8 +39,5 @@
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}"
when:
- result is succeeded
- change_info is succeeded
register: result
failed_when: not result.is_available

View File

@@ -35,12 +35,24 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
password: "{{ account.secret | password_hash('des') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -57,19 +69,9 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,6 +101,8 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

@@ -5,6 +5,12 @@ type:
- AIX
method: push_account
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
en: 'Using Ansible module user to push account (DES)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -35,12 +35,24 @@
- user_info.failed
- params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password"
ansible.builtin.user:
name: "{{ account.username }}"
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -57,19 +69,9 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection
ansible.builtin.meta: reset_connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,6 +101,8 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

@@ -6,6 +6,12 @@ type:
- linux
method: push_account
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
en: 'Using Ansible module user to push account (sha512)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@@ -28,4 +28,6 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change

View File

@@ -31,5 +31,7 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change
delegate_to: localhost

View File

@@ -12,11 +12,13 @@
path: "{{ user_home_dir.stdout }}"
register: home_dir
when: user_home_dir.stdout != ""
ignore_errors: yes
- name: "Rename user home directory if it exists"
ansible.builtin.command:
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
when: home_dir.stat | default(false) and user_home_dir.stdout != ""
ignore_errors: yes
- name: "Remove account"
ansible.builtin.user:

View File

@@ -16,3 +16,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
register: result
failed_when: not result.is_available

View File

@@ -8,6 +8,7 @@
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
ansible_timeout: 30
when: not account.become.ansible_become
- name: Verify account connectivity(Switch)
@@ -20,4 +21,5 @@
ansible_become_method: "{{ account.become.ansible_become_method }}"
ansible_become_user: "{{ account.become.ansible_become_user }}"
ansible_become_password: "{{ account.become.ansible_become_password }}"
ansible_timeout: 30
when: account.become.ansible_become

View File

@@ -9,3 +9,4 @@
vars:
ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}"
ansible_timeout: 30

View File

@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
on_delete=models.SET_NULL, verbose_name=_("Su from")
)
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
verbose_name=_("historical Account"))
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
@@ -119,7 +120,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
return auth
auth.update(self.make_account_ansible_vars(su_from))
become_method = platform.su_method if platform.su_method else 'sudo'
become_method = platform.ansible_become_method
password = su_from.secret if become_method == 'sudo' else self.secret
auth['ansible_become'] = True
auth['ansible_become_method'] = become_method

View File

@@ -79,18 +79,28 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
@staticmethod
def get_template_attr_for_account(template):
# Set initial data from template
field_names = [
'name', 'username', 'secret',
'secret_type', 'privileged', 'is_active'
'name', 'username',
'secret_type', 'secret',
'privileged', 'is_active'
]
field_map = {
'push_params': 'params',
'auto_push': 'push_now'
}
field_names.extend(field_map.keys())
attrs = {}
for name in field_names:
value = getattr(template, name, None)
if value is None:
continue
attrs[name] = value
attr_name = field_map.get(name, name)
attrs[attr_name] = value
attrs['secret'] = template.get_secret()
return attrs
@@ -173,7 +183,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
params = validated_data.pop('params', None)
self.clean_auth_fields(validated_data)
instance, stat = self.do_create(validated_data)
self.push_account_if_need(instance, push_now, params, stat)
if instance.source == Source.LOCAL:
self.push_account_if_need(instance, push_now, params, stat)
return instance
def update(self, instance, validated_data):
@@ -225,7 +236,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
fields = BaseAccountSerializer.Meta.fields + [
'su_from', 'asset', 'version',
'source', 'source_id', 'connectivity',
] + list(set(AccountCreateUpdateSerializerMixin.Meta.fields) - {'params'})
] + AccountCreateUpdateSerializerMixin.Meta.fields
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
'connectivity'
]
@@ -275,8 +286,8 @@ class AssetAccountBulkSerializer(
fields = [
'name', 'username', 'secret', 'secret_type', 'passphrase',
'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'assets', 'su_from_username',
'source', 'source_id',
'on_invalid', 'push_now', 'params', 'assets',
'su_from_username', 'source', 'source_id',
]
extra_kwargs = {
'name': {'required': False},
@@ -414,16 +425,23 @@ class AssetAccountBulkSerializer(
return results
@staticmethod
def push_accounts_if_need(results, push_now):
def push_accounts_if_need(results, push_now, params):
if not push_now:
return
accounts = [str(v['instance']) for v in results if v.get('instance')]
push_accounts_to_assets_task.delay(accounts)
account_ids = [v['instance'] for v in results if v.get('instance')]
accounts = Account.objects.filter(id__in=account_ids, source=Source.LOCAL)
if not accounts.exists():
return
account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
push_accounts_to_assets_task.delay(account_ids, params)
def create(self, validated_data):
params = validated_data.pop('params', None)
push_now = validated_data.pop('push_now', False)
results = self.perform_bulk_create(validated_data)
self.push_accounts_if_need(results, push_now)
self.push_accounts_if_need(results, push_now, params)
for res in results:
res['asset'] = str(res['asset'])
return results

View File

@@ -6,6 +6,7 @@ from django.dispatch import receiver
from django.utils.translation import gettext_noop
from accounts.backends import vault_client
from accounts.const import Source
from audits.const import ActivityChoices
from audits.signal_handlers import create_activities
from common.decorators import merge_delay_run
@@ -32,7 +33,7 @@ def push_accounts_if_need(accounts=()):
template_accounts = defaultdict(list)
for ac in accounts:
# 再强调一次吧
if ac.source != 'template':
if ac.source != Source.TEMPLATE:
continue
template_accounts[ac.source_id].append(ac)
@@ -61,7 +62,7 @@ def create_accounts_activities(account, action='create'):
@receiver(post_save, sender=Account)
def on_account_create_by_template(sender, instance, created=False, **kwargs):
if not created or instance.source != 'template':
if not created:
return
push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create')

View File

@@ -2,10 +2,11 @@ import copy
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
from common.utils import ssh_key_gen, random_string
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
from common.utils import (
validate_ssh_private_key, parse_ssh_private_key_str, ssh_key_gen,
random_string
)
class SecretGenerator:

View File

@@ -1,5 +1,7 @@
# coding: utf-8
#
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -8,7 +10,7 @@ from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
logger = get_logger(__file__)
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text']
__all__ = ['RuleSerializer', 'ip_group_child_validator', 'ip_group_help_text', 'address_validator']
def ip_group_child_validator(ip_group_child):
@@ -21,6 +23,19 @@ def ip_group_child_validator(ip_group_child):
raise serializers.ValidationError(error)
def address_validator(value):
parsed = urlparse(value)
is_basic_url = parsed.scheme in ('http', 'https') and parsed.netloc
is_valid = value == '*' \
or is_ip_address(value) \
or is_ip_network(value) \
or is_ip_segment(value) \
or is_basic_url
if not is_valid:
error = _('address invalid: `{}`').format(value)
raise serializers.ValidationError(error)
ip_group_help_text = _(
'With * indicating a match all. '
'Such as: '

View File

@@ -292,6 +292,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
def check_permissions(self, request):
action_perm_require = {
"refresh": "assets.refresh_assethardwareinfo",
"test": "assets.test_assetconnectivity",
}
_action = request.data.get("action")
perm_required = action_perm_require.get(_action)

View File

@@ -39,16 +39,16 @@ class NodeChildrenApi(generics.ListCreateAPIView):
self.instance = self.get_object()
def perform_create(self, serializer):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if value:
children = self.instance.get_children()
if children.filter(value=value).exists():
raise JMSException(_('The same level node name cannot be the same'))
else:
value = self.instance.get_next_child_preset_name()
with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if value:
children = self.instance.get_children()
if children.filter(value=value).exists():
raise JMSException(_('The same level node name cannot be the same'))
else:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value

View File

@@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
from sshtunnel import SSHTunnelForwarder
from assets.automations.methods import platform_automation_methods
from common.db.utils import safe_db_connection
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
from ops.ansible import JMSInventory, DefaultCallback, SuperPlaybookRunner
from ops.ansible.interface import interface
@@ -113,11 +114,7 @@ class BasePlaybookManager:
if not data:
data = automation_params.get(method_id, {})
params = serializer(data).data
return {
field_name: automation_params.get(field_name, '')
if not params[field_name] else params[field_name]
for field_name in params
}
return params
@property
def platform_automation_methods(self):
@@ -191,6 +188,7 @@ class BasePlaybookManager:
host['error'] = _('{} disabled'.format(self.__class__.method_type()))
return host
host['check_conn_after_change'] = settings.CHECK_CONN_AFTER_CHANGE
host = self.convert_cert_to_file(host, kwargs.get('path_dir'))
host['params'] = self.get_params(automation, method_type)
return host
@@ -342,7 +340,8 @@ class BasePlaybookManager:
try:
kwargs.update({"clean_workspace": False})
cb = runner.run(**kwargs)
self.on_runner_success(runner, cb)
with safe_db_connection():
self.on_runner_success(runner, cb)
except Exception as e:
self.on_runner_failed(runner, e)
finally:

View File

@@ -1,3 +1,5 @@
from collections import Counter
__all__ = ['FormatAssetInfo']
@@ -7,13 +9,28 @@ class FormatAssetInfo:
self.tp = tp
@staticmethod
def posix_format(info):
for cpu_model in info.get('cpu_model', []):
if cpu_model.endswith('GHz') or cpu_model.startswith("Intel"):
break
else:
cpu_model = ''
info['cpu_model'] = cpu_model[:48]
def get_cpu_model_count(cpus):
try:
if len(cpus) % 3 == 0:
step = 3
models = [cpus[i + 2] for i in range(0, len(cpus), step)]
elif len(cpus) % 2 == 0:
step = 2
models = [cpus[i + 1] for i in range(0, len(cpus), step)]
else:
raise ValueError("CPU list format not recognized")
model_counts = Counter(models)
result = ', '.join([f"{model} x{count}" for model, count in model_counts.items()])
except Exception as e:
print(f"Error processing CPU model list: {e}")
result = ''
return result
def posix_format(self, info):
cpus = self.get_cpu_model_count(info.get('cpu_model', []))
info['cpu_model'] = cpus
info['cpu_count'] = info.get('cpu_count', 0)
return info

View File

@@ -16,3 +16,5 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
register: result
failed_when: not result.is_available

View File

@@ -1,5 +1,7 @@
- hosts: demo
gather_facts: no
vars:
ansible_timeout: 30
tasks:
- name: Posix ping
ansible.builtin.ping:

View File

@@ -1,5 +1,7 @@
- hosts: windows
gather_facts: no
vars:
ansible_timeout: 30
tasks:
- name: Refresh connection
ansible.builtin.meta: reset_connection

View File

@@ -2,5 +2,6 @@ from .automation import *
from .base import *
from .category import *
from .host import *
from .platform import *
from .protocol import *
from .types import *

View File

@@ -37,6 +37,7 @@ class DatabaseTypes(BaseType):
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True,
'remove_account_enabled': True,
},
cls.REDIS: {
'ansible_enabled': False,

View File

@@ -19,7 +19,7 @@ class HostTypes(BaseType):
'charset': 'utf-8', # default
'domain_enabled': True,
'su_enabled': True,
'su_methods': ['sudo', 'su'],
'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
},
cls.WINDOWS: {
'su_enabled': False,
@@ -53,7 +53,8 @@ class HostTypes(BaseType):
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True
'push_account_enabled': True,
'remove_account_enabled': True,
},
cls.WINDOWS: {
'ansible_config': {

View File

@@ -0,0 +1,11 @@
from django.db.models import TextChoices
class SuMethodChoices(TextChoices):
sudo = "sudo", "sudo su -"
su = "su", "su - "
only_sudo = "only_sudo", "sudo su"
only_su = "only_su", "su"
enable = "enable", "enable"
super = "super", "super 15"
super_level = "super_level", "super level 15"

View File

@@ -208,6 +208,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': 'admin',
'label': _('Auth source'),
'help_text': _('The database to authenticate against')
},
'connection_options': {
'type': 'str',
'default': '',
'label': _('Connection options'),
'help_text': _('The connection specific options eg. retryWrites=false&retryReads=false')
}
}
},
@@ -289,23 +295,17 @@ class Protocol(ChoicesMixin, models.TextChoices):
'setting': {
'api_mode': {
'type': 'choice',
'default': 'gpt-3.5-turbo',
'default': 'gpt-4o-mini',
'label': _('API mode'),
'choices': [
('gpt-3.5-turbo', 'GPT-3.5 Turbo'),
('gpt-3.5-turbo-1106', 'GPT-3.5 Turbo 1106'),
('gpt-4o-mini', 'GPT-4o-mini'),
('gpt-4o', 'GPT-4o'),
('gpt-4-turbo', 'GPT-4 Turbo'),
]
}
}
}
}
if settings.XPACK_LICENSE_IS_VALID:
choices = protocols[cls.chatgpt]['setting']['api_mode']['choices']
choices.extend([
('gpt-4', 'GPT-4'),
('gpt-4-turbo', 'GPT-4 Turbo'),
('gpt-4o', 'GPT-4o'),
])
return protocols
@classmethod

View File

@@ -1,10 +1,12 @@
# Generated by Django 3.2.16 on 2022-12-30 08:08
import common.db.fields
from django.db import migrations, models
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.db import migrations, models
import common.db.fields
class Migration(migrations.Migration):
@@ -53,7 +55,7 @@ class Migration(migrations.Migration):
],
options={
'verbose_name': 'Automation task execution',
'ordering': ('-date_start',),
'ordering': ('org_id', '-date_start',),
},
),
migrations.CreateModel(

View File

@@ -174,7 +174,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
def get_labels(self):
from labels.models import Label, LabeledResource
res_type = ContentType.objects.get_for_model(self.__class__)
res_type = ContentType.objects.get_for_model(self.__class__.label_model())
label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \
.values_list('label_id', flat=True)
return Label.objects.filter(id__in=label_ids)

View File

@@ -1,7 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from assets.const import AllTypes, Category, Protocol
from assets.const import AllTypes, Category, Protocol, SuMethodChoices
from common.db.fields import JsonDictTextField
from common.db.models import JMSBaseModel
@@ -127,6 +127,17 @@ class Platform(LabeledMixin, JMSBaseModel):
return True
return False
@property
def ansible_become_method(self):
su_method = self.su_method or SuMethodChoices.sudo
if su_method in [SuMethodChoices.sudo, SuMethodChoices.only_sudo]:
method = SuMethodChoices.sudo
elif su_method in [SuMethodChoices.su, SuMethodChoices.only_su]:
method = SuMethodChoices.su
else:
method = su_method
return method
def __str__(self):
return self.name

View File

@@ -323,7 +323,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
template_id = data.get('template', None)
if template_id:
template = AccountTemplate.objects.get(id=template_id)
if template and template.su_from:
template.push_params = data.pop('push_params', {})
data['params'] = template.push_params
if template.su_from:
su_from_name_username_secret_type_map[template.name] = (
template.su_from.username, template.su_from.secret_type
)

View File

@@ -41,7 +41,7 @@ class DatabaseSerializer(AssetSerializer):
elif self.context.get('request'):
platform_id = self.context['request'].query_params.get('platform')
if not platform and platform_id:
if not platform and platform_id and str(platform_id).isdigit():
platform = Platform.objects.filter(id=platform_id).first()
return platform

View File

@@ -9,7 +9,7 @@ from common.serializers import (
)
from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty
from ..const import Category, AllTypes, Protocol
from ..const import Category, AllTypes, Protocol, SuMethodChoices
from ..models import Platform, PlatformProtocol, PlatformAutomation
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"]
@@ -27,6 +27,7 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
"change_secret_enabled", "change_secret_method", "change_secret_params",
"verify_account_enabled", "verify_account_method", "verify_account_params",
"gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
"remove_account_enabled", "remove_account_method", "remove_account_params",
]
extra_kwargs = {
# 启用资产探测
@@ -42,6 +43,8 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
"push_account_method": {"label": _("Push account method")},
"gather_accounts_enabled": {"label": _("Gather accounts enabled")},
"gather_accounts_method": {"label": _("Gather accounts method")},
"remove_account_method": {"label": _("Remove account method")},
"remove_account_enabled": {"label": _("Remove account enabled")},
}
@@ -124,13 +127,6 @@ class PlatformCustomField(serializers.Serializer):
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
SU_METHOD_CHOICES = [
("sudo", "sudo su -"),
("su", "su - "),
("enable", "enable"),
("super", "super 15"),
("super_level", "super level 15")
]
id = serializers.IntegerField(
label='ID', required=False,
validators=[UniqueValidator(queryset=Platform.objects.all())]
@@ -141,8 +137,8 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField(
choices=SU_METHOD_CHOICES, label=_("Su method"),
required=False, default="sudo", allow_null=True
choices=SuMethodChoices.choices, label=_("Su method"),
required=False, default=SuMethodChoices.sudo, allow_null=True
)
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)

View File

@@ -28,7 +28,7 @@ from orgs.utils import current_org, tmp_to_root_org
from rbac.permissions import RBACPermission
from terminal.models import default_storage
from users.models import User
from .backends import TYPE_ENGINE_MAPPING
from .backends import get_operate_log_storage
from .const import ActivityChoices
from .filters import UserSessionFilterSet, OperateLogFilterSet
from .models import (
@@ -146,7 +146,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
username = self.request.user.username
q = Q(username=username) | Q(username__icontains=f'({username})')
qs = qs.filter(q)
return qs
@@ -187,9 +189,13 @@ class ResourceActivityAPIView(generics.ListAPIView):
'id', 'datetime', 'r_detail', 'r_detail_id',
'r_user', 'r_action', 'r_type'
)
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
org_q = Q()
if not current_org.is_root():
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
with tmp_to_root_org():
qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id)
qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id)
@@ -222,13 +228,11 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
if self.is_action_detail:
with tmp_to_root_org():
qs |= OperateLog.objects.filter(org_id=Organization.SYSTEM_ID)
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
store = engine_mod.OperateLogStore(es_config)
if store.ping(timeout=2):
qs = ESQuerySet(store)
qs.model = OperateLog
storage = get_operate_log_storage()
if storage.get_type() == 'es':
qs = ESQuerySet(storage)
qs.model = OperateLog
return qs

View File

@@ -1,18 +1,62 @@
from importlib import import_module
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger
from .base import BaseOperateStorage
from .es import OperateLogStore as ESOperateLogStore
from .db import OperateLogStore as DBOperateLogStore
TYPE_ENGINE_MAPPING = {
'db': 'audits.backends.db',
'es': 'audits.backends.es',
logger = get_logger(__file__)
_global_op_log_storage: None | ESOperateLogStore | DBOperateLogStore = None
op_log_type_mapping = {
'server': DBOperateLogStore, 'es': ESOperateLogStore
}
def get_operate_log_storage(default=False):
engine_mod = import_module(TYPE_ENGINE_MAPPING['db'])
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if not default and es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
storage = engine_mod.OperateLogStore(es_config)
return storage
def _send_es_unavailable_alarm_msg():
from terminal.notifications import StorageConnectivityMessage
from terminal.const import CommandStorageType
key = 'OPERATE_LOG_ES_UNAVAILABLE_KEY'
if cache.get(key):
return
cache.set(key, 1, 60)
errors = [{
'msg': _("Connect failed"), 'name': f"{_('Operate log')}",
'type': CommandStorageType.es.label
}]
StorageConnectivityMessage(errors).publish_async()
def refresh_log_storage():
global _global_op_log_storage
_global_op_log_storage = None
if settings.OPERATE_LOG_ELASTICSEARCH_CONFIG.get('HOSTS'):
try:
config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
log_storage = op_log_type_mapping['es'](config)
_global_op_log_storage = log_storage
except Exception as e:
_send_es_unavailable_alarm_msg()
logger.warning('Invalid logs storage type: es, error: %s' % str(e))
if not _global_op_log_storage:
_global_op_log_storage = op_log_type_mapping['server']()
def get_operate_log_storage():
if _global_op_log_storage is None:
refresh_log_storage()
log_storage = _global_op_log_storage
if not log_storage.ping(timeout=3):
if log_storage.get_type() == 'es':
_send_es_unavailable_alarm_msg()
logger.warning('Switch default operate log storage.')
log_storage = op_log_type_mapping['server']()
return log_storage

View File

@@ -0,0 +1,15 @@
from perms.const import ActionChoices
class BaseOperateStorage(object):
@staticmethod
def get_type():
return 'base'
@staticmethod
def _get_special_handler(resource_type):
# 根据资源类型,处理特殊字段
resource_map = {
'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v
}
return resource_map.get(resource_type, lambda k, v: v)

View File

@@ -2,14 +2,14 @@
from django.utils.translation import gettext_lazy as _
from audits.models import OperateLog
from perms.const import ActionChoices
from .base import BaseOperateStorage
class OperateLogStore(object):
class OperateLogStore(BaseOperateStorage):
# 用不可见字符分割前后数据,节省存储-> diff: {'key': 'before\0after'}
SEP = '\0'
def __init__(self, config):
def __init__(self, *args, **kwargs):
self.model = OperateLog
self.max_length = 2048
self.max_length_tip_msg = _(
@@ -17,9 +17,13 @@ class OperateLogStore(object):
)
@staticmethod
def ping(timeout=None):
def ping(*args, **kwargs):
return True
@staticmethod
def get_type():
return 'db'
@classmethod
def convert_before_after_to_diff(cls, before, after):
if not isinstance(before, dict):
@@ -46,18 +50,13 @@ class OperateLogStore(object):
before[k], after[k] = before_value, after_value
return before, after
@staticmethod
def _get_special_handler(resource_type):
# 根据资源类型,处理特殊字段
resource_map = {
'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v
}
return resource_map.get(resource_type, lambda k, v: _(v))
@classmethod
def convert_diff_friendly(cls, op_log):
diff_list = list()
handler = cls._get_special_handler(op_log.resource_type)
# 标记翻译字符串
labels = _("labels")
operate_log_id = _("operate_log_id")
for k, v in op_log.diff.items():
before, after = v.split(cls.SEP, 1)
diff_list.append({

View File

@@ -2,16 +2,17 @@
#
import uuid
from django.utils.translation import gettext_lazy as _
from common.utils.timezone import local_now_display
from common.utils import get_logger
from common.utils.encode import Singleton
from common.plugins.es import ES
from .base import BaseOperateStorage
logger = get_logger(__file__)
class OperateLogStore(ES, metaclass=Singleton):
class OperateLogStore(BaseOperateStorage, ES):
def __init__(self, config):
properties = {
"id": {
@@ -48,7 +49,26 @@ class OperateLogStore(ES, metaclass=Singleton):
self.pre_use_check()
@staticmethod
def make_data(data):
def get_type():
return 'es'
@classmethod
def convert_diff_friendly(cls, op_log):
diff_list = []
handler = cls._get_special_handler(op_log.get('resource_type'))
before = op_log.get('before') or {}
after = op_log.get('after') or {}
keys = set(before.keys()) | set(after.keys())
for key in keys:
before_v, after_v = before.get(key), after.get(key)
diff_list.append({
'field': _(key),
'before': handler(key, before_v) if before_v else _('empty'),
'after': handler(key, after_v) if after_v else _('empty'),
})
return diff_list
def make_data(self, data):
op_id = data.get('id', str(uuid.uuid4()))
datetime_param = data.get('datetime', local_now_display())
data = {

View File

@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
from common.local import encrypted_field_set
from common.utils import get_request_ip, get_logger
from common.utils.encode import Singleton
from common.utils.timezone import as_current_tz
from jumpserver.utils import current_request
from orgs.models import Organization
@@ -21,17 +20,9 @@ from .backends import get_operate_log_storage
logger = get_logger(__name__)
class OperatorLogHandler(metaclass=Singleton):
class OperatorLogHandler(object):
CACHE_KEY = 'OPERATOR_LOG_CACHE_KEY'
def __init__(self):
self.log_client = self.get_storage_client()
@staticmethod
def get_storage_client():
client = get_operate_log_storage()
return client
@staticmethod
def _consistent_type_to_str(value1, value2):
if isinstance(value1, datetime):
@@ -164,13 +155,8 @@ class OperatorLogHandler(metaclass=Singleton):
'remote_addr': remote_addr, 'before': before, 'after': after,
}
with transaction.atomic():
if self.log_client.ping(timeout=1):
client = self.log_client
else:
logger.info('Switch default operate log storage save.')
client = get_operate_log_storage(default=True)
try:
client = get_operate_log_storage()
client.save(**data)
except Exception as e:
error_msg = 'An error occurred saving OperateLog.' \

View File

@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='operatelog',
name='action',
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('accept', 'Accept'), ('review', 'Review'), ('notice', 'Notifications'), ('reject', 'Reject'), ('approve', 'Approve'), ('close', 'Close')], max_length=16, verbose_name='Action'),
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password'), ('accept', 'Accept'), ('review', 'Review'), ('notice', 'Notifications'), ('reject', 'Reject'), ('approve', 'Approve'), ('close', 'Close'), ('finished', 'Finished')], max_length=16, verbose_name='Action'),
),
migrations.AlterField(
model_name='userloginlog',

View File

@@ -3,7 +3,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from audits.backends.db import OperateLogStore
from audits.backends import get_operate_log_storage
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import reverse, i18n_trans
from common.utils.timezone import as_current_tz
@@ -77,7 +77,7 @@ class OperateLogActionDetailSerializer(serializers.ModelSerializer):
fields = ('diff',)
def to_representation(self, instance):
return {'diff': OperateLogStore.convert_diff_friendly(instance)}
return {'diff': get_operate_log_storage().convert_diff_friendly(instance)}
class OperateLogSerializer(BulkOrgResourceModelSerializer):

View File

@@ -14,7 +14,7 @@ from audits.handler import (
create_or_update_operate_log, get_instance_dict_from_cache
)
from audits.utils import model_to_dict_for_operate_log as model_to_dict
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, OP_LOG_SKIP_SIGNAL
from common.signals import django_ready
from jumpserver.utils import current_request
from ..const import MODELS_NEED_RECORD, ActionChoices
@@ -77,7 +77,7 @@ def signal_of_operate_log_whether_continue(
condition = True
if not instance:
condition = False
if instance and getattr(instance, SKIP_SIGNAL, False):
if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False):
condition = False
# 不记录组件的操作日志
user = current_request.user if current_request else None
@@ -189,7 +189,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord'
}
include_models = {'UserSession'}
include_models = set()
for i, app in enumerate(apps.get_models(), 1):
app_name = app._meta.app_label
model_name = app._meta.object_name

View File

@@ -16,6 +16,7 @@ from common.storage.ftp_file import FTPFileStorageHandler
from common.utils import get_log_keep_day, get_logger
from ops.celery.decorator import register_as_period_task
from ops.models import CeleryTaskExecution
from orgs.utils import tmp_to_root_org
from terminal.backends import server_replay_storage
from terminal.models import Session, Command
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog
@@ -105,8 +106,9 @@ def clean_expired_session_period():
logger.info("Clean session item done")
batch_delete(expired_commands)
logger.info("Clean session command done")
command = "find %s -mtime +%s \\( -name '*.json' -o -name '*.tar' -o -name '*.gz' \\) -exec rm -f {} \\;" % (
replay_dir, days
file_types = "-name '*.json' -o -name '*.tar' -o -name '*.gz' -o -name '*.mp4'"
command = "find %s -mtime +%s \\( %s \\) -exec rm -f {} \\;" % (
replay_dir, days, file_types
)
subprocess.call(command, shell=True)
command = "find %s -type d -empty -delete;" % replay_dir
@@ -118,13 +120,14 @@ def clean_expired_session_period():
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clean_audits_log_period():
print("Start clean audit session task log")
clean_login_log_period()
clean_operation_log_period()
clean_ftp_log_period()
clean_activity_log_period()
clean_celery_tasks_period()
clean_expired_session_period()
clean_password_change_log_period()
with tmp_to_root_org():
clean_login_log_period()
clean_operation_log_period()
clean_ftp_log_period()
clean_activity_log_period()
clean_celery_tasks_period()
clean_expired_session_period()
clean_password_change_log_period()
@shared_task(verbose_name=_('Upload FTP file to external storage'))

View File

@@ -472,6 +472,8 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'list': 'authentication.view_superconnectiontoken',
'retrieve': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -479,7 +481,12 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
}
def get_queryset(self):
return ConnectionToken.objects.all()
return ConnectionToken.objects.none()
def get_object(self):
pk = self.kwargs.get(self.lookup_field)
token = get_object_or_404(ConnectionToken, pk=pk)
return token
def get_user(self, serializer):
return serializer.validated_data.get('user')

View File

@@ -14,7 +14,6 @@ from rest_framework.response import Response
from authentication.errors import ACLError
from common.api import JMSGenericViewSet
from common.const.http import POST, GET
from common.permissions import OnlySuperUser
from common.serializers import EmptySerializer
from common.utils import reverse, safe_next_url
from common.utils.timezone import utc_now
@@ -38,8 +37,11 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
'login_url': SSOTokenSerializer,
'login': EmptySerializer
}
@action(methods=[POST], detail=False, permission_classes=[OnlySuperUser], url_path='login-url')
rbac_perms = {
'login_url': 'authentication.add_ssotoken',
}
@action(methods=[POST], detail=False, url_path='login-url')
def login_url(self, request, *args, **kwargs):
if not settings.AUTH_SSO:
raise SSOAuthClosed()
@@ -103,11 +105,9 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
self.request.session['auth_backend'] = settings.AUTH_BACKEND_SSO
login(self.request, user, settings.AUTH_BACKEND_SSO)
self.send_auth_signal(success=True, user=user)
self.mark_mfa_ok('otp', user)
LoginIpBlockUtil(ip).clean_block_if_need()
LoginBlockUtil(username, ip).clean_failed_count()
self.clear_auth_mark()
except (ACLError, LoginConfirmBaseError): # 无需记录日志
pass
except (AuthFailedError, SSOAuthKeyTTLError) as e:

View File

@@ -128,7 +128,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
# example implementation:
try:
key = AccessKey.objects.get(id=key_id)
if not key.is_active:
if not key.is_valid:
return None, None
user, secret = key.user, str(key.secret)
after_authenticate_update_date(user, key)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import base64
import requests
from django.utils.translation import gettext_lazy as _
@@ -67,14 +68,6 @@ class OAuth2Backend(JMSModelBackend):
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'))
@@ -83,29 +76,31 @@ class OAuth2Backend(JMSModelBackend):
return None
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'grant_type': 'authorization_code', 'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT:
separator = '&'
else:
separator = '?'
separator = '&' if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT else '?'
access_token_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, separator=separator, query=urlencode(query_dict)
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT,
separator=separator, query=urlencode(query_dict)
)
# token_method -> get, post(post_data), post_json
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
encoded_credentials = base64.b64encode(
f"{settings.AUTH_OAUTH2_CLIENT_ID}:{settings.AUTH_OAUTH2_CLIENT_SECRET}".encode()
).decode()
headers = {
'Accept': 'application/json'
'Accept': 'application/json', 'Authorization': f'Basic {encoded_credentials}'
}
if token_method.startswith('post'):
body_key = 'json' if token_method.endswith('json') else 'data'
query_dict.update({
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
})
access_token_response = requests.post(
access_token_url, headers=headers, **{body_key: query_dict}
)
@@ -121,22 +116,12 @@ class OAuth2Backend(JMSModelBackend):
logger.error(log_prompt.format(error))
return None
query_dict = self.get_query_dict(response_data, query_dict)
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
if '?' in settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT:
separator = '&'
else:
separator = '?'
userinfo_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT, separator=separator,
query=urlencode(query_dict)
)
userinfo_url = settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
userinfo_response = requests.get(userinfo_url, headers=headers)
try:
userinfo_response.raise_for_status()

View File

@@ -107,7 +107,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# parameters because we won't be able to get a valid token for the user in that case.
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
logger.debug(log_prompt.format('Authorization code or state value is missing'))
raise SuspiciousOperation('Authorization code or state value is missing')
return
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
@@ -165,7 +165,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
@@ -206,7 +206,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
return
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)

View File

@@ -1,14 +1,12 @@
import copy
from urllib import parse
from django.views import View
from django.contrib import auth
from django.urls import reverse
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from django.contrib import auth
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError
from django.urls import reverse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.errors import OneLogin_Saml2_Error
from onelogin.saml2.idp_metadata_parser import (
@@ -16,23 +14,29 @@ from onelogin.saml2.idp_metadata_parser import (
dict_deep_merge
)
from .settings import JmsSaml2Settings
from common.utils import get_logger
from .settings import JmsSaml2Settings
logger = get_logger(__file__)
class PrepareRequestMixin:
@staticmethod
def is_secure():
url_result = parse.urlparse(settings.SITE_URL)
return 'on' if url_result.scheme == 'https' else 'off'
@property
def parsed_url(self):
return parse.urlparse(settings.SITE_URL)
def is_secure(self):
return 'on' if self.parsed_url.scheme == 'https' else 'off'
def http_host(self):
return f"{self.parsed_url.hostname}:{self.parsed_url.port}" \
if self.parsed_url.port else self.parsed_url.hostname
def prepare_django_request(self, request):
result = {
'https': self.is_secure(),
'http_host': request.META['HTTP_HOST'],
'http_host': self.http_host(),
'script_name': request.META['PATH_INFO'],
'get_data': request.GET.copy(),
'post_data': request.POST.copy()
@@ -275,7 +279,7 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Redirect'))
redir = post_data.get('RelayState')
if not redir or len(redir) == 0:
redir = "/"
redir = "/"
next_url = saml_instance.redirect_to(redir)
return HttpResponseRedirect(next_url)

View File

@@ -2,6 +2,7 @@ import base64
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.http import HttpResponse
from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
@@ -35,7 +36,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
'/profile/otp', '/logout/',
'/profile/otp', '/logout/', '/media/'
]
for url in white_urls:
if request.path.find(url) > -1:
@@ -76,6 +77,7 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
ip = get_request_ip(request)
try:
self.request = request
self.check_is_block()
self._check_third_party_login_acl()
self._check_login_acl(request.user, ip)
except Exception as e:
@@ -116,23 +118,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
@staticmethod
def set_cookie_public_key(request, response):
def set_cookie_public_key(self, request, response):
if request.path.startswith('/api'):
return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
session_public_key = request.session.get(session_public_key_name)
cookie_public_key = request.COOKIES.get(session_public_key_name)
if session_public_key and session_public_key == cookie_public_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
private_key, public_key = self.get_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
request.session[session_public_key_name] = public_key_decode
request.session[session_private_key_name] = private_key
response.set_cookie(session_public_key_name, public_key_decode)
def get_key_pair(self):
key_pair = cache.get(self.USER_LOGIN_ENCRYPTION_KEY_PAIR)
if key_pair:
return key_pair['private_key'], key_pair['public_key']
private_key, public_key = gen_key_pair()
key_pair = {
'private_key': private_key,
'public_key': public_key
}
cache.set(self.USER_LOGIN_ENCRYPTION_KEY_PAIR, key_pair, None)
return private_key, public_key
@staticmethod
def set_cookie_session_prefix(request, response):

View File

@@ -319,20 +319,26 @@ class AuthPostCheckMixin:
@classmethod
def _check_passwd_is_too_simple(cls, user: User, password):
if user.is_superuser and password == 'admin':
if not user.is_auth_backend_model():
return
if user.check_passwd_too_simple(password):
message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
raise errors.PasswordTooSimple(url)
@classmethod
def _check_passwd_need_update(cls, user: User):
if user.need_update_password:
if not user.is_auth_backend_model():
return
if user.check_need_update_password():
message = _('You should to change your password before login')
url = cls.generate_reset_password_url_with_flash_msg(user, message)
raise errors.PasswordNeedUpdate(url)
@classmethod
def _check_password_require_reset_or_not(cls, user: User):
if not user.is_auth_backend_model():
return
if user.password_has_expired:
message = _('Your password has expired, please reset before logging in')
url = cls.generate_reset_password_url_with_flash_msg(user, message)

View File

@@ -3,3 +3,4 @@ from .connection_token import *
from .private_token import *
from .sso_token import *
from .temp_token import *
from ..backends.passkey.models import *

View File

@@ -26,6 +26,10 @@ class AccessKey(models.Model):
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
date_created = models.DateTimeField(auto_now_add=True)
@property
def is_valid(self):
return self.is_active and self.user.is_valid
def get_id(self):
return str(self.id)

View File

@@ -200,7 +200,7 @@ class ConnectionToken(JMSOrgBaseModel):
host_account = applet.select_host_account(self.user, self.asset)
if not host_account:
raise JMSException({'error': 'No host account available'})
raise JMSException({'error': 'No host account available, please check the applet, host and account'})
host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key'))
gateway = host.domain.select_gateway() if host.domain else None

View File

@@ -8,10 +8,8 @@
<p>
<b>{% trans 'Username' %}:</b> {{ username }}<br>
<b>{% trans 'Login time' %}:</b> {{ time }}<br>
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})<br>
</p>
-
<p>
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
</p>

View File

@@ -10,8 +10,7 @@
{% trans 'Click here reset password' %}
</a>
</p>
-
<br>
<p>
{% trans 'This link is valid for 1 hour. After it expires' %}
<a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>

View File

@@ -5,11 +5,10 @@
{% trans 'Your password has just been successfully updated' %}
</p>
<p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
<b>{% trans 'Browser' %}:</b> {{ browser }}
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
<b>{% trans 'Browser' %}:</b> {{ browser }} <br>
</p>
-
<p>
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br />
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br/>
{% trans 'If you have any questions, you can contact the administrator' %}
</p>

View File

@@ -5,11 +5,10 @@
{% trans 'Your public key has just been successfully updated' %}
</p>
<p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br />
<b>{% trans 'Browser' %}:</b> {{ browser }}
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
<b>{% trans 'Browser' %}:</b> {{ browser }}<br>
</p>
-
<p>
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br />
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br/>
{% trans 'If you have any questions, you can contact the administrator' %}
</p>

View File

@@ -396,7 +396,7 @@
</body>
{% include '_foot_js.html' %}
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.3.3.2.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/cryptojs/crypto-js.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/buffer/buffer.min.js"></script>
<script>

View File

@@ -1,4 +1,4 @@
from .utils import gen_key_pair, rsa_decrypt, rsa_encrypt
from common.utils import gen_key_pair, rsa_decrypt, rsa_encrypt
def test_rsa_encrypt_decrypt(message='test-password-$%^&*'):

View File

@@ -46,9 +46,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
def verify_state(self):
raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def create_user_if_not_exist(self, user_id, **kwargs):
user = None
user_attr = self.client.get_user_detail(user_id, **kwargs)
@@ -122,9 +119,6 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
def verify_state(self):
raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def get_already_bound_response(self, redirect_uri):
raise NotImplementedError
@@ -151,11 +145,9 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
setattr(user, f'{self.auth_type}_id', auth_user_id)
user.save()
except IntegrityError as e:
if e.args[0] == 1062:
msg = _('The %s is already bound to another user') % self.auth_type_label
response = self.get_failed_response(redirect_url, msg, msg)
return response
raise e
msg = _('The %s is already bound to another user') % self.auth_type_label
response = self.get_failed_response(redirect_url, msg, msg)
return response
ip = get_request_ip(request)
OAuthBindMessage(user, ip, self.auth_type_label, auth_user_id).publish_async()

View File

@@ -47,15 +47,7 @@ class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, Fla
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(DINGTALK_STATE_SESSION_KEY)
def get_already_bound_response(self, redirect_url):
msg = _('DingTalk is already bound')

View File

@@ -58,15 +58,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(self.state_session_key)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(self.state_session_key)
def get_qr_url(self, redirect_uri):
state = random_string(16)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import gettext_lazy as _
from common.utils import FlashMessageUtil
@@ -32,3 +33,12 @@ class FlashMessageMixin:
def get_failed_response(self, redirect_url, title, msg, interval=10):
return self.get_response(redirect_url, title, msg, 'error', interval)
def get_verify_state_failed_response(self, redirect_uri):
msg = _(
"For your safety, automatic redirection login is not supported on the client."
" If you need to open it in the client, please log in again")
return self.get_failed_response(redirect_uri, msg, msg)
def verify_state_with_session_key(self, session_key):
return self.request.GET.get('state') == self.request.session.get(session_key)

View File

@@ -37,15 +37,7 @@ class SlackMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessa
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(SLACK_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(SLACK_STATE_SESSION_KEY)
def get_qr_url(self, redirect_uri):
state = random_string(16)

View File

@@ -45,15 +45,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
)
def verify_state(self):
state = self.request.GET.get('state')
session_state = self.request.session.get(WECOM_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound')

View File

@@ -8,6 +8,8 @@ from rest_framework.request import Request
from rest_framework.response import Response
from common.const.http import POST
from orgs.models import Organization
from orgs.utils import current_org
__all__ = ['SuggestionMixin', 'RenderToJsonMixin']
@@ -23,7 +25,16 @@ class SuggestionMixin:
@action(methods=['get'], detail=False, url_path='suggestions')
def match(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
queryset = self.get_queryset()
org_id = str(current_org.id)
if (
not request.user.is_superuser and
org_id != Organization.ROOT_ID and
not request.user.orgs.filter(id=org_id).exists()
):
queryset = queryset.none()
queryset = self.filter_queryset(queryset)
queryset = queryset[:self.suggestion_limit]
page = self.paginate_queryset(queryset)

View File

@@ -16,4 +16,4 @@ POST_CLEAR = 'post_clear'
POST_PREFIX = 'post'
PRE_PREFIX = 'pre'
SKIP_SIGNAL = 'skip_signal'
OP_LOG_SKIP_SIGNAL = 'operate_log_skip_signal'

View File

@@ -17,7 +17,7 @@ from django.db.models import F, ExpressionWrapper, CASCADE
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from ..const.signals import SKIP_SIGNAL
from ..const.signals import OP_LOG_SKIP_SIGNAL
class ChoicesMixin:
@@ -82,7 +82,7 @@ def CASCADE_SIGNAL_SKIP(collector, field, sub_objs, using):
# 级联删除时,操作日志标记不保存,以免用户混淆
try:
for obj in sub_objs:
setattr(obj, SKIP_SIGNAL, True)
setattr(obj, OP_LOG_SKIP_SIGNAL, True)
except:
pass

View File

@@ -70,7 +70,8 @@ known_unauth_urls = [
"/api/v1/authentication/login-confirm-ticket/status/",
"/api/v1/authentication/mfa/select/",
"/api/v1/authentication/mfa/send-code/",
"/api/v1/authentication/sso/login/"
"/api/v1/authentication/sso/login/",
"/api/v1/authentication/user-session/"
]
known_error_urls = [
@@ -91,7 +92,6 @@ class Command(BaseCommand):
unauth_urls = []
error_urls = []
unformat_urls = []
for url, ourl in urls:
if '(' in url or '<' in url:
unformat_urls.append([url, ourl])

View File

@@ -190,7 +190,8 @@ class ES(object):
mappings['aliases'] = {
self.query_index: {}
}
if self.es.indices.exists(index=self.index):
return
try:
self.es.indices.create(index=self.index, body=mappings)
except (RequestError, BadRequestError) as e:

View File

@@ -1,6 +1,7 @@
cipher_alg_id = {
"sm4_ebc": 0x00000401,
"sm4_cbc": 0x00000402,
"sm4_mac": 0x00000405,
}

View File

@@ -75,7 +75,7 @@ class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
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')
service_id = service_id.ljust(10, '\x00').encode('utf-8')
fee_user_type = struct.pack('!B', 2)
fee_terminal_id = ('0' * 21).encode('utf-8')
tp_pid = struct.pack('!B', 0)
@@ -85,7 +85,7 @@ class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
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')
src_id = src_id.ljust(21, '\x00').encode('utf-8')
reserve = b'\x00' * 8
_msg_length = struct.pack('!B', len(msg_content) * 2)
_msg_src = msg_src.encode('utf-8')

View File

@@ -39,8 +39,7 @@ class CustomSMS(BaseSMSClient):
kwargs = {'params': params}
try:
response = action(url=settings.CUSTOM_SMS_URL, verify=False, **kwargs)
if response.reason != 'OK':
raise JMSException(detail=response.text, code=response.status_code)
response.raise_for_status()
except Exception as exc:
logger.error('Custom sms error: {}'.format(exc))
raise JMSException(exc)

View File

@@ -51,6 +51,19 @@ def date_expired_default():
years = 70
return timezone.now() + timezone.timedelta(days=365 * years)
def user_date_expired_default():
try:
days = int(settings.USER_DEFAULT_EXPIRED_DAYS)
except TypeError:
days = 25550
return timezone.now() + timezone.timedelta(days=days)
def asset_permission_date_expired_default():
try:
days = int(settings.ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS)
except TypeError:
days = 25550
return timezone.now() + timezone.timedelta(days=days)
def union_queryset(*args, base_queryset=None):
if len(args) == 1:

View File

@@ -21,6 +21,7 @@ def i18n_fmt(tpl, *args):
return tpl
args = [str(arg) for arg in args]
args = [arg.replace(', ', ' ') for arg in args]
try:
tpl % tuple(args)

View File

@@ -13,9 +13,9 @@ from common.utils.random import random_string
logger = get_logger(__file__)
@shared_task(verbose_name=_('Send email'))
def send_async(sender):
sender.gen_and_send()
@shared_task(verbose_name=_('Send SMS code'))
def send_sms_async(target, code):
SMS().send_verify_code(target, code)
class SendAndVerifyCodeUtil(object):
@@ -35,7 +35,7 @@ class SendAndVerifyCodeUtil(object):
logger.warning('Send sms too frequently, delay {}'.format(ttl))
raise CodeSendTooFrequently(ttl)
return send_async.apply_async(kwargs={"sender": self}, priority=100)
return self.gen_and_send()
def gen_and_send(self):
try:
@@ -72,13 +72,15 @@ class SendAndVerifyCodeUtil(object):
return code
def __send_with_sms(self):
sms = SMS()
sms.send_verify_code(self.target, self.code)
send_sms_async.apply_async(args=(self.target, self.code), priority=100)
def __send_with_email(self):
subject = self.other_args.get('subject')
message = self.other_args.get('message')
send_mail_async(subject, message, [self.target], html_message=message)
subject = self.other_args.get('subject', '')
message = self.other_args.get('message', '')
send_mail_async.apply_async(
args=(subject, message, [self.target]),
kwargs={'html_message': message}, priority=100
)
def __send(self, code):
"""

View File

@@ -106,7 +106,7 @@ class DateTimeMixin:
@lazyproperty
def user_login_logs_on_the_system_queryset(self):
qs = UserLoginLog.objects.all()
qs = UserLoginLog.objects.filter(status=LoginStatusChoices.success)
qs = self.get_logs_queryset_filter(qs, 'datetime')
queryset = qs.filter(username__in=construct_userlogin_usernames(self.users))
return queryset

View File

@@ -229,11 +229,14 @@ class Config(dict):
'TOKEN_EXPIRATION': 3600 * 24,
'DEFAULT_EXPIRED_YEARS': 70,
'USER_DEFAULT_EXPIRED_DAYS': 25550,
'ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS': 25550,
'SESSION_COOKIE_DOMAIN': None,
'CSRF_COOKIE_DOMAIN': None,
'SESSION_COOKIE_NAME_PREFIX': None,
'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'VIEW_ASSET_ONLINE_SESSION_INFO': True,
'LOGIN_URL': reverse_lazy('authentication:login'),
'CONNECTION_TOKEN_ONETIME_EXPIRATION': 5 * 60, # 默认(new)
@@ -487,6 +490,7 @@ class Config(dict):
'TERMINAL_OMNIDB_ENABLED': True,
# 安全配置
'CHECK_CONN_AFTER_CHANGE': True,
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True,
'SECURITY_COMMAND_EXECUTION': False,
@@ -600,7 +604,6 @@ class Config(dict):
# API 分页
'MAX_LIMIT_PER_PAGE': 10000,
'DEFAULT_PAGE_SIZE': None,
'LIMIT_SUPER_PRIV': False,

View File

@@ -3,13 +3,16 @@
import json
import os
import re
import time
from urllib.parse import urlparse, quote
import pytz
import time
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.http.response import HttpResponseForbidden
from django.shortcuts import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
from .utils import set_current_request
@@ -137,3 +140,38 @@ class EndMiddleware:
response = self.get_response(request)
request._e_time_end = time.time()
return response
class SafeRedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if not (300 <= response.status_code < 400):
return response
if request.resolver_match and request.resolver_match.namespace.startswith('authentication'):
# 认证相关的路由跳过验证core/auth/xxxx
return response
location = response.get('Location')
if not location:
return response
parsed = urlparse(location)
if parsed.scheme and parsed.netloc:
target_host = parsed.netloc
if target_host in [*settings.ALLOWED_HOSTS]:
return response
target_host, target_port = self._split_host_port(parsed.netloc)
origin_host, origin_port = self._split_host_port(request.get_host())
if target_host != origin_host:
safe_redirect_url = '%s?%s' % (reverse('redirect-confirm'), f'next={quote(location)}')
return redirect(safe_redirect_url)
return response
@staticmethod
def _split_host_port(netloc):
if ':' in netloc:
host, port = netloc.split(':', 1)
return host, port
return netloc, '80'

View File

@@ -11,7 +11,10 @@ current_year = datetime.datetime.now().year
corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}'
XPACK_DIR = os.path.join(const.BASE_DIR, 'xpack')
XPACK_ENABLED = os.path.isdir(XPACK_DIR)
XPACK_DISABLED = os.environ.get('XPACK_ENABLED') in ['0', 'false', 'False', 'no', 'No']
XPACK_ENABLED = False
if not XPACK_DISABLED:
XPACK_ENABLED = os.path.isdir(XPACK_DIR)
XPACK_TEMPLATES_DIR = []
XPACK_CONTEXT_PROCESSOR = []
XPACK_LICENSE_IS_VALID = False

View File

@@ -181,6 +181,7 @@ MIDDLEWARE = [
'authentication.middleware.ThirdPartyLoginMiddleware',
'authentication.middleware.SessionCookieMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
'jumpserver.middleware.SafeRedirectMiddleware',
'jumpserver.middleware.EndMiddleware',
]
@@ -236,6 +237,7 @@ SESSION_COOKIE_NAME = '{}sessionid'.format(SESSION_COOKIE_NAME_PREFIX)
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE
VIEW_ASSET_ONLINE_SESSION_INFO = CONFIG.VIEW_ASSET_ONLINE_SESSION_INFO
SESSION_ENGINE = "common.sessions.{}".format(CONFIG.SESSION_ENGINE)
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'

View File

@@ -32,6 +32,8 @@ TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE
FTP_FILE_MAX_STORE = CONFIG.FTP_FILE_MAX_STORE
# Security settings
CHECK_CONN_AFTER_CHANGE = CONFIG.CHECK_CONN_AFTER_CHANGE
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY = CONFIG.SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
@@ -115,7 +117,9 @@ EMAIL_CUSTOM_USER_CREATED_BODY = CONFIG.EMAIL_CUSTOM_USER_CREATED_BODY
EMAIL_CUSTOM_USER_CREATED_SIGNATURE = CONFIG.EMAIL_CUSTOM_USER_CREATED_SIGNATURE
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
DEFAULT_EXPIRED_YEARS = 70
DEFAULT_EXPIRED_YEARS = CONFIG.DEFAULT_EXPIRED_YEARS
USER_DEFAULT_EXPIRED_DAYS = CONFIG.USER_DEFAULT_EXPIRED_DAYS
ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS = CONFIG.ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS
USER_GUIDE_URL = CONFIG.USER_GUIDE_URL
HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
@@ -208,7 +212,7 @@ SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG
MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE
DEFAULT_PAGE_SIZE = CONFIG.DEFAULT_PAGE_SIZE
DEFAULT_PAGE_SIZE = CONFIG.MAX_LIMIT_PER_PAGE
PERM_TREE_REGEN_INTERVAL = CONFIG.PERM_TREE_REGEN_INTERVAL
# Magnus DB Port

View File

@@ -39,6 +39,7 @@ app_view_patterns = [
path('common/', include('common.urls.view_urls'), name='common'),
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
path('download/', views.ResourceDownload.as_view(), name='download'),
path('redirect/confirm/', views.RedirectConfirm.as_view(), name='redirect-confirm'),
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
]

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