Compare commits

...

61 Commits

Author SHA1 Message Date
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
104 changed files with 11439 additions and 1702 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 TARGETARCH
ARG VERSION ARG VERSION

View File

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

View File

@@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - 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" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" 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 - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

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

View File

@@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - 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" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" 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 - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

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

View File

@@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - 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" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" 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 - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

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

View File

@@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - 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" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" 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 - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

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

View File

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

View File

@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
on_delete=models.SET_NULL, verbose_name=_("Su from") on_delete=models.SET_NULL, verbose_name=_("Su from")
) )
version = models.IntegerField(default=0, verbose_name=_('Version')) 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 = 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')) 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 return auth
auth.update(self.make_account_ansible_vars(su_from)) 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 password = su_from.secret if become_method == 'sudo' else self.secret
auth['ansible_become'] = True auth['ansible_become'] = True
auth['ansible_become_method'] = become_method auth['ansible_become_method'] = become_method

View File

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

View File

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

View File

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

View File

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

View File

@@ -113,11 +113,7 @@ class BasePlaybookManager:
if not data: if not data:
data = automation_params.get(method_id, {}) data = automation_params.get(method_id, {})
params = serializer(data).data params = serializer(data).data
return { return params
field_name: automation_params.get(field_name, '')
if not params[field_name] else params[field_name]
for field_name in params
}
@property @property
def platform_automation_methods(self): def platform_automation_methods(self):

View File

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

View File

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

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', 'default': 'admin',
'label': _('Auth source'), 'label': _('Auth source'),
'help_text': _('The database to authenticate against') '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': { 'setting': {
'api_mode': { 'api_mode': {
'type': 'choice', 'type': 'choice',
'default': 'gpt-3.5-turbo', 'default': 'gpt-4o-mini',
'label': _('API mode'), 'label': _('API mode'),
'choices': [ 'choices': [
('gpt-3.5-turbo', 'GPT-3.5 Turbo'), ('gpt-4o-mini', 'GPT-4o-mini'),
('gpt-3.5-turbo-1106', 'GPT-3.5 Turbo 1106'), ('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 return protocols
@classmethod @classmethod

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ 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.fields import JsonDictTextField
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
@@ -127,6 +127,17 @@ class Platform(LabeledMixin, JMSBaseModel):
return True return True
return False 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): def __str__(self):
return self.name return self.name

View File

@@ -323,7 +323,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
template_id = data.get('template', None) template_id = data.get('template', None)
if template_id: if template_id:
template = AccountTemplate.objects.get(id=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] = ( su_from_name_username_secret_type_map[template.name] = (
template.su_from.username, template.su_from.secret_type template.su_from.username, template.su_from.secret_type
) )

View File

@@ -9,7 +9,7 @@ from common.serializers import (
) )
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty from common.utils import lazyproperty
from ..const import Category, AllTypes, Protocol from ..const import Category, AllTypes, Protocol, SuMethodChoices
from ..models import Platform, PlatformProtocol, PlatformAutomation from ..models import Platform, PlatformProtocol, PlatformAutomation
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"] __all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"]
@@ -124,13 +124,6 @@ class PlatformCustomField(serializers.Serializer):
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer): 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( id = serializers.IntegerField(
label='ID', required=False, label='ID', required=False,
validators=[UniqueValidator(queryset=Platform.objects.all())] validators=[UniqueValidator(queryset=Platform.objects.all())]
@@ -141,8 +134,8 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False) protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict) automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField( su_method = LabeledChoiceField(
choices=SU_METHOD_CHOICES, label=_("Su method"), choices=SuMethodChoices.choices, label=_("Su method"),
required=False, default="sudo", allow_null=True required=False, default=SuMethodChoices.sudo, allow_null=True
) )
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False) 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 rbac.permissions import RBACPermission
from terminal.models import default_storage from terminal.models import default_storage
from users.models import User from users.models import User
from .backends import TYPE_ENGINE_MAPPING from .backends import get_operate_log_storage
from .const import ActivityChoices from .const import ActivityChoices
from .filters import UserSessionFilterSet, OperateLogFilterSet from .filters import UserSessionFilterSet, OperateLogFilterSet
from .models import ( from .models import (
@@ -146,7 +146,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() 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 return qs
@@ -222,12 +224,10 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
if self.is_action_detail: if self.is_action_detail:
with tmp_to_root_org(): with tmp_to_root_org():
qs |= OperateLog.objects.filter(org_id=Organization.SYSTEM_ID) qs |= OperateLog.objects.filter(org_id=Organization.SYSTEM_ID)
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if es_config: storage = get_operate_log_storage()
engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) if storage.get_type() == 'es':
store = engine_mod.OperateLogStore(es_config) qs = ESQuerySet(storage)
if store.ping(timeout=2):
qs = ESQuerySet(store)
qs.model = OperateLog qs.model = OperateLog
return qs return qs

View File

@@ -1,18 +1,62 @@
from importlib import import_module
from django.conf import settings 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 = { logger = get_logger(__file__)
'db': 'audits.backends.db',
'es': 'audits.backends.es', _global_op_log_storage: None | ESOperateLogStore | DBOperateLogStore = None
op_log_type_mapping = {
'server': DBOperateLogStore, 'es': ESOperateLogStore
} }
def get_operate_log_storage(default=False): def _send_es_unavailable_alarm_msg():
engine_mod = import_module(TYPE_ENGINE_MAPPING['db']) from terminal.notifications import StorageConnectivityMessage
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG from terminal.const import CommandStorageType
if not default and es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) key = 'OPERATE_LOG_ES_UNAVAILABLE_KEY'
storage = engine_mod.OperateLogStore(es_config) if cache.get(key):
return storage 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 django.utils.translation import gettext_lazy as _
from audits.models import OperateLog from audits.models import OperateLog
from perms.const import ActionChoices from .base import BaseOperateStorage
class OperateLogStore(object): class OperateLogStore(BaseOperateStorage):
# 用不可见字符分割前后数据,节省存储-> diff: {'key': 'before\0after'} # 用不可见字符分割前后数据,节省存储-> diff: {'key': 'before\0after'}
SEP = '\0' SEP = '\0'
def __init__(self, config): def __init__(self, *args, **kwargs):
self.model = OperateLog self.model = OperateLog
self.max_length = 2048 self.max_length = 2048
self.max_length_tip_msg = _( self.max_length_tip_msg = _(
@@ -17,9 +17,13 @@ class OperateLogStore(object):
) )
@staticmethod @staticmethod
def ping(timeout=None): def ping(*args, **kwargs):
return True return True
@staticmethod
def get_type():
return 'db'
@classmethod @classmethod
def convert_before_after_to_diff(cls, before, after): def convert_before_after_to_diff(cls, before, after):
if not isinstance(before, dict): if not isinstance(before, dict):
@@ -46,18 +50,13 @@ class OperateLogStore(object):
before[k], after[k] = before_value, after_value before[k], after[k] = before_value, after_value
return before, after 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 @classmethod
def convert_diff_friendly(cls, op_log): def convert_diff_friendly(cls, op_log):
diff_list = list() diff_list = list()
handler = cls._get_special_handler(op_log.resource_type) 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(): for k, v in op_log.diff.items():
before, after = v.split(cls.SEP, 1) before, after = v.split(cls.SEP, 1)
diff_list.append({ diff_list.append({

View File

@@ -2,16 +2,17 @@
# #
import uuid import uuid
from django.utils.translation import gettext_lazy as _
from common.utils.timezone import local_now_display from common.utils.timezone import local_now_display
from common.utils import get_logger from common.utils import get_logger
from common.utils.encode import Singleton
from common.plugins.es import ES from common.plugins.es import ES
from .base import BaseOperateStorage
logger = get_logger(__file__) logger = get_logger(__file__)
class OperateLogStore(ES, metaclass=Singleton): class OperateLogStore(BaseOperateStorage, ES):
def __init__(self, config): def __init__(self, config):
properties = { properties = {
"id": { "id": {
@@ -48,7 +49,26 @@ class OperateLogStore(ES, metaclass=Singleton):
self.pre_use_check() self.pre_use_check()
@staticmethod @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())) op_id = data.get('id', str(uuid.uuid4()))
datetime_param = data.get('datetime', local_now_display()) datetime_param = data.get('datetime', local_now_display())
data = { data = {

View File

@@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
from common.local import encrypted_field_set from common.local import encrypted_field_set
from common.utils import get_request_ip, get_logger from common.utils import get_request_ip, get_logger
from common.utils.encode import Singleton
from common.utils.timezone import as_current_tz from common.utils.timezone import as_current_tz
from jumpserver.utils import current_request from jumpserver.utils import current_request
from orgs.models import Organization from orgs.models import Organization
@@ -21,17 +20,9 @@ from .backends import get_operate_log_storage
logger = get_logger(__name__) logger = get_logger(__name__)
class OperatorLogHandler(metaclass=Singleton): class OperatorLogHandler(object):
CACHE_KEY = 'OPERATOR_LOG_CACHE_KEY' 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 @staticmethod
def _consistent_type_to_str(value1, value2): def _consistent_type_to_str(value1, value2):
if isinstance(value1, datetime): if isinstance(value1, datetime):
@@ -164,13 +155,8 @@ class OperatorLogHandler(metaclass=Singleton):
'remote_addr': remote_addr, 'before': before, 'after': after, 'remote_addr': remote_addr, 'before': before, 'after': after,
} }
with transaction.atomic(): 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: try:
client = get_operate_log_storage()
client.save(**data) client.save(**data)
except Exception as e: except Exception as e:
error_msg = 'An error occurred saving OperateLog.' \ error_msg = 'An error occurred saving OperateLog.' \

View File

@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='operatelog', model_name='operatelog',
name='action', 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( migrations.AlterField(
model_name='userloginlog', model_name='userloginlog',

View File

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

View File

@@ -189,7 +189,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'ApplyCommandTicket', 'ApplyLoginAssetTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket',
'FavoriteAsset', 'ChangeSecretRecord' 'FavoriteAsset', 'ChangeSecretRecord'
} }
include_models = {'UserSession'} include_models = set()
for i, app in enumerate(apps.get_models(), 1): for i, app in enumerate(apps.get_models(), 1):
app_name = app._meta.app_label app_name = app._meta.app_label
model_name = app._meta.object_name 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 common.utils import get_log_keep_day, get_logger
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import register_as_period_task
from ops.models import CeleryTaskExecution from ops.models import CeleryTaskExecution
from orgs.utils import tmp_to_root_org
from terminal.backends import server_replay_storage from terminal.backends import server_replay_storage
from terminal.models import Session, Command from terminal.models import Session, Command
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog
@@ -105,8 +106,9 @@ def clean_expired_session_period():
logger.info("Clean session item done") logger.info("Clean session item done")
batch_delete(expired_commands) batch_delete(expired_commands)
logger.info("Clean session command done") logger.info("Clean session command done")
command = "find %s -mtime +%s \\( -name '*.json' -o -name '*.tar' -o -name '*.gz' \\) -exec rm -f {} \\;" % ( file_types = "-name '*.json' -o -name '*.tar' -o -name '*.gz' -o -name '*.mp4'"
replay_dir, days command = "find %s -mtime +%s \\( %s \\) -exec rm -f {} \\;" % (
replay_dir, days, file_types
) )
subprocess.call(command, shell=True) subprocess.call(command, shell=True)
command = "find %s -type d -empty -delete;" % replay_dir command = "find %s -type d -empty -delete;" % replay_dir
@@ -118,6 +120,7 @@ def clean_expired_session_period():
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO) @register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clean_audits_log_period(): def clean_audits_log_period():
print("Start clean audit session task log") print("Start clean audit session task log")
with tmp_to_root_org():
clean_login_log_period() clean_login_log_period()
clean_operation_log_period() clean_operation_log_period()
clean_ftp_log_period() clean_ftp_log_period()

View File

@@ -1,14 +1,12 @@
import copy import copy
from urllib import parse 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.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.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.auth import OneLogin_Saml2_Auth
from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.errors import OneLogin_Saml2_Error
from onelogin.saml2.idp_metadata_parser import ( from onelogin.saml2.idp_metadata_parser import (
@@ -16,23 +14,29 @@ from onelogin.saml2.idp_metadata_parser import (
dict_deep_merge dict_deep_merge
) )
from .settings import JmsSaml2Settings
from common.utils import get_logger from common.utils import get_logger
from .settings import JmsSaml2Settings
logger = get_logger(__file__) logger = get_logger(__file__)
class PrepareRequestMixin: class PrepareRequestMixin:
@staticmethod
def is_secure(): @property
url_result = parse.urlparse(settings.SITE_URL) def parsed_url(self):
return 'on' if url_result.scheme == 'https' else 'off' 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): def prepare_django_request(self, request):
result = { result = {
'https': self.is_secure(), 'https': self.is_secure(),
'http_host': request.META['HTTP_HOST'], 'http_host': self.http_host(),
'script_name': request.META['PATH_INFO'], 'script_name': request.META['PATH_INFO'],
'get_data': request.GET.copy(), 'get_data': request.GET.copy(),
'post_data': request.POST.copy() 'post_data': request.POST.copy()

View File

@@ -2,6 +2,7 @@ import base64
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout as auth_logout from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, reverse, render from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
@@ -116,23 +117,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
class SessionCookieMiddleware(MiddlewareMixin): class SessionCookieMiddleware(MiddlewareMixin):
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
@staticmethod def set_cookie_public_key(self, request, response):
def set_cookie_public_key(request, response):
if request.path.startswith('/api'): if request.path.startswith('/api'):
return return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name) session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
cookie_key = request.COOKIES.get(pub_key_name) session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
if public_key and public_key == cookie_key:
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 return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME private_key, public_key = self.get_key_pair()
private_key, public_key = gen_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode() public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key request.session[session_public_key_name] = public_key_decode
response.set_cookie(pub_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 @staticmethod
def set_cookie_session_prefix(request, response): def set_cookie_session_prefix(request, response):

View File

@@ -319,20 +319,26 @@ class AuthPostCheckMixin:
@classmethod @classmethod
def _check_passwd_is_too_simple(cls, user: User, password): 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') message = _('Your password is too simple, please change it for security')
url = cls.generate_reset_password_url_with_flash_msg(user, message=message) url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
raise errors.PasswordTooSimple(url) raise errors.PasswordTooSimple(url)
@classmethod @classmethod
def _check_passwd_need_update(cls, user: User): 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') message = _('You should to change your password before login')
url = cls.generate_reset_password_url_with_flash_msg(user, message) url = cls.generate_reset_password_url_with_flash_msg(user, message)
raise errors.PasswordNeedUpdate(url) raise errors.PasswordNeedUpdate(url)
@classmethod @classmethod
def _check_password_require_reset_or_not(cls, user: User): def _check_password_require_reset_or_not(cls, user: User):
if not user.is_auth_backend_model():
return
if user.password_has_expired: if user.password_has_expired:
message = _('Your password has expired, please reset before logging in') message = _('Your password has expired, please reset before logging in')
url = cls.generate_reset_password_url_with_flash_msg(user, message) 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 .private_token import *
from .sso_token import * from .sso_token import *
from .temp_token import * from .temp_token import *
from ..backends.passkey.models import *

View File

@@ -200,7 +200,7 @@ class ConnectionToken(JMSOrgBaseModel):
host_account = applet.select_host_account(self.user, self.asset) host_account = applet.select_host_account(self.user, self.asset)
if not host_account: 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')) host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key'))
gateway = host.domain.select_gateway() if host.domain else None gateway = host.domain.select_gateway() if host.domain else None

View File

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

View File

@@ -10,8 +10,7 @@
{% trans 'Click here reset password' %} {% trans 'Click here reset password' %}
</a> </a>
</p> </p>
<br>
-
<p> <p>
{% trans 'This link is valid for 1 hour. After it expires' %} {% trans 'This link is valid for 1 hour. After it expires' %}
<a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a> <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' %} {% trans 'Your password has just been successfully updated' %}
</p> </p>
<p> <p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br /> <b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
<b>{% trans 'Browser' %}:</b> {{ browser }} <b>{% trans 'Browser' %}:</b> {{ browser }} <br>
</p> </p>
-
<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' %} {% trans 'If you have any questions, you can contact the administrator' %}
</p> </p>

View File

@@ -5,11 +5,10 @@
{% trans 'Your public key has just been successfully updated' %} {% trans 'Your public key has just been successfully updated' %}
</p> </p>
<p> <p>
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br /> <b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
<b>{% trans 'Browser' %}:</b> {{ browser }} <b>{% trans 'Browser' %}:</b> {{ browser }}<br>
</p> </p>
-
<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' %} {% trans 'If you have any questions, you can contact the administrator' %}
</p> </p>

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-$%^&*'): def test_rsa_encrypt_decrypt(message='test-password-$%^&*'):

View File

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

View File

@@ -58,15 +58,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(self.state_session_key)
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)
def get_qr_url(self, redirect_uri): def get_qr_url(self, redirect_uri):
state = random_string(16) state = random_string(16)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import gettext_lazy as _
from common.utils import FlashMessageUtil from common.utils import FlashMessageUtil
@@ -32,3 +33,12 @@ class FlashMessageMixin:
def get_failed_response(self, redirect_url, title, msg, interval=10): def get_failed_response(self, redirect_url, title, msg, interval=10):
return self.get_response(redirect_url, title, msg, 'error', interval) 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): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(SLACK_STATE_SESSION_KEY)
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)
def get_qr_url(self, redirect_uri): def get_qr_url(self, redirect_uri):
state = random_string(16) state = random_string(16)

View File

@@ -45,15 +45,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
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)
def get_already_bound_response(self, redirect_url): def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound') msg = _('WeCom is already bound')

View File

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

View File

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

View File

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

View File

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

View File

@@ -106,7 +106,7 @@ class DateTimeMixin:
@lazyproperty @lazyproperty
def user_login_logs_on_the_system_queryset(self): 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') qs = self.get_logs_queryset_filter(qs, 'datetime')
queryset = qs.filter(username__in=construct_userlogin_usernames(self.users)) queryset = qs.filter(username__in=construct_userlogin_usernames(self.users))
return queryset return queryset

View File

@@ -600,7 +600,6 @@ class Config(dict):
# API 分页 # API 分页
'MAX_LIMIT_PER_PAGE': 10000, 'MAX_LIMIT_PER_PAGE': 10000,
'DEFAULT_PAGE_SIZE': None,
'LIMIT_SUPER_PRIV': False, 'LIMIT_SUPER_PRIV': False,

View File

@@ -11,7 +11,10 @@ current_year = datetime.datetime.now().year
corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}' corporation = f'FIT2CLOUD 飞致云 © 2014-{current_year}'
XPACK_DIR = os.path.join(const.BASE_DIR, 'xpack') 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_TEMPLATES_DIR = []
XPACK_CONTEXT_PROCESSOR = [] XPACK_CONTEXT_PROCESSOR = []
XPACK_LICENSE_IS_VALID = False XPACK_LICENSE_IS_VALID = False

View File

@@ -208,7 +208,7 @@ SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG
MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE 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 PERM_TREE_REGEN_INTERVAL = CONFIG.PERM_TREE_REGEN_INTERVAL
# Magnus DB Port # Magnus DB Port

View File

@@ -121,7 +121,7 @@ class SSHClient:
def local_gateway_prepare(self): def local_gateway_prepare(self):
gateway_args = self.module.params['gateway_args'] or '' gateway_args = self.module.params['gateway_args'] or ''
pattern = r"(?:sshpass -p ([\w@]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \ pattern = r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \
r"\d.]+)\s+-W %h:%p -q(?: -i (.+))?'" r"\d.]+)\s+-W %h:%p -q(?: -i (.+))?'"
match = re.search(pattern, gateway_args) match = re.search(pattern, gateway_args)

View File

@@ -19,7 +19,7 @@ def kill_ansible_ssh_process(pid):
for child in process.children(recursive=True): for child in process.children(recursive=True):
if not _should_kill(child): if not _should_kill(child):
return continue
try: try:
child.kill() child.kill()
except Exception as e: except Exception as e:

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845b68b6d2cdd03b1c1a939d8f1238446731988ab052c66f7c7f6946af7ae406
size 431

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:375a5bd7b21f2ddd004a228ef1debcf81e32812a4392128e0acf184f3317428d
size 380

View File

@@ -0,0 +1,103 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-10 18:22+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/jumpserver.js:264
msgid "Update is successful!"
msgstr ""
#: static/js/jumpserver.js:266
msgid "An unknown error occurred while updating.."
msgstr ""
#: static/js/jumpserver.js:339
msgid "Not found"
msgstr ""
#: static/js/jumpserver.js:341
msgid "Server error"
msgstr ""
#: static/js/jumpserver.js:343 static/js/jumpserver.js:381
#: static/js/jumpserver.js:383
msgid "Error"
msgstr ""
#: static/js/jumpserver.js:349 static/js/jumpserver.js:390
msgid "Delete the success"
msgstr ""
#: static/js/jumpserver.js:356
msgid "Are you sure about deleting it?"
msgstr ""
#: static/js/jumpserver.js:360 static/js/jumpserver.js:401
msgid "Cancel"
msgstr ""
#: static/js/jumpserver.js:362 static/js/jumpserver.js:403
msgid "Confirm"
msgstr ""
#: static/js/jumpserver.js:381
msgid ""
"The organization contains undeleted information. Please try again after "
"deleting"
msgstr ""
#: static/js/jumpserver.js:383
msgid ""
"Do not perform this operation under this organization. Try again after "
"switching to another organization"
msgstr ""
#: static/js/jumpserver.js:397
msgid ""
"Please ensure that the following information in the organization has been "
"deleted"
msgstr ""
#: static/js/jumpserver.js:398
msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission"
msgstr ""
#: static/js/jumpserver.js:647
msgid "Unknown error occur"
msgstr ""
#: static/js/jumpserver.js:899
msgid "Password minimum length {N} bits"
msgstr ""
#: static/js/jumpserver.js:900
msgid "Must contain capital letters"
msgstr ""
#: static/js/jumpserver.js:901
msgid "Must contain lowercase letters"
msgstr ""
#: static/js/jumpserver.js:902
msgid "Must contain numeric characters"
msgstr ""
#: static/js/jumpserver.js:903
msgid "Must contain special characters"
msgstr ""

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:52990de6b508e55b8b5f4a70f86c567410c5cf217ca312847f65178393d81b19 oid sha256:2dd9ffcfe15b130a5b3d7b4fcfe806eaae979973e8bd29ad9a473b9215424c57
size 177824 size 178725

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1c4a4fa3abb21fea213011d50fd62455fb1ddf73538401dc8cd9c03f4f4bbc77 oid sha256:76ecc817b74abdf4f274425444339c575b723c76800095733176f9b7dada052e
size 3322 size 2465

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-22 15:29+0800\n" "POT-Creation-Date: 2024-07-10 18:22+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,141 +18,134 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
#: static/js/jumpserver.js:260 #: static/js/jumpserver.js:264
msgid "Update is successful!" msgid "Update is successful!"
msgstr "アップデートは成功しました!" msgstr "アップデートは成功しました!"
#: static/js/jumpserver.js:262 #: static/js/jumpserver.js:266
msgid "An unknown error occurred while updating.." msgid "An unknown error occurred while updating.."
msgstr "更新中に不明なエラーが発生しました。" msgstr "更新中に不明なエラーが発生しました。"
#: static/js/jumpserver.js:333 #: static/js/jumpserver.js:339
msgid "Not found" msgid "Not found"
msgstr "見つかりません" msgstr "見つかりません"
#: static/js/jumpserver.js:335 #: static/js/jumpserver.js:341
msgid "Server error" msgid "Server error"
msgstr "サーバーエラー" msgstr "サーバーエラー"
#: static/js/jumpserver.js:337 static/js/jumpserver.js:375 #: static/js/jumpserver.js:343 static/js/jumpserver.js:381
#: static/js/jumpserver.js:377 #: static/js/jumpserver.js:383
msgid "Error" msgid "Error"
msgstr "エラー" msgstr "エラー"
#: static/js/jumpserver.js:343 static/js/jumpserver.js:384 #: static/js/jumpserver.js:349 static/js/jumpserver.js:390
msgid "Delete the success" msgid "Delete the success"
msgstr "成功を削除する" msgstr "成功を削除する"
#: static/js/jumpserver.js:350 #: static/js/jumpserver.js:356
msgid "Are you sure about deleting it?" msgid "Are you sure about deleting it?"
msgstr "削除してもよろしいですか?" msgstr "削除してもよろしいですか?"
#: static/js/jumpserver.js:354 static/js/jumpserver.js:395 #: static/js/jumpserver.js:360 static/js/jumpserver.js:401
msgid "Cancel" msgid "Cancel"
msgstr "キャンセル" msgstr "キャンセル"
#: static/js/jumpserver.js:356 static/js/jumpserver.js:397 #: static/js/jumpserver.js:362 static/js/jumpserver.js:403
msgid "Confirm" msgid "Confirm"
msgstr "確認" msgstr "確認"
#: static/js/jumpserver.js:375 #: static/js/jumpserver.js:381
msgid "" msgid ""
"The organization contains undeleted information. Please try again after " "The organization contains undeleted information. Please try again after "
"deleting" "deleting"
msgstr "組織には削除されていない情報が含まれています。削除後にもう一度お試しください" msgstr ""
"組織には削除されていない情報が含まれています。削除後にもう一度お試しください"
#: static/js/jumpserver.js:377 #: static/js/jumpserver.js:383
msgid "" msgid ""
"Do not perform this operation under this organization. Try again after " "Do not perform this operation under this organization. Try again after "
"switching to another organization" "switching to another organization"
msgstr "この組織ではこの操作を実行しないでください。別の組織に切り替えた後にもう一度お試しください" msgstr ""
"この組織ではこの操作を実行しないでください。別の組織に切り替えた後にもう一度"
"お試しください"
#: static/js/jumpserver.js:391 #: static/js/jumpserver.js:397
msgid "" msgid ""
"Please ensure that the following information in the organization has been " "Please ensure that the following information in the organization has been "
"deleted" "deleted"
msgstr "組織内の次の情報が削除されていることを確認してください" msgstr "組織内の次の情報が削除されていることを確認してください"
#: static/js/jumpserver.js:392 #: static/js/jumpserver.js:398
msgid "" msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、" "User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission" "Labels、Asset permission"
msgstr "ユーザーリスト、ユーザーグループ、資産リスト、ドメインリスト、管理ユーザー、システムユーザー、ラベル、資産権限" msgstr ""
"ユーザーリスト、ユーザーグループ、資産リスト、ドメインリスト、管理ユーザー、"
"システムユーザー、ラベル、資産権限"
#: static/js/jumpserver.js:469 #: static/js/jumpserver.js:647
msgid "Loading"
msgstr "読み込み中"
#: static/js/jumpserver.js:470
msgid "Search"
msgstr "検索"
#: static/js/jumpserver.js:473
#, javascript-format
msgid "Selected item %d"
msgstr "選択したアイテム % d"
#: static/js/jumpserver.js:477
msgid "Per page _MENU_"
msgstr "各ページ _MENU_"
#: static/js/jumpserver.js:478
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "アイテムの結果を表示します _START_ に着く _END_; 合計 _TOTAL_ エントリ"
#: static/js/jumpserver.js:481
msgid "No match"
msgstr "一致しません"
#: static/js/jumpserver.js:482
msgid "No record"
msgstr "記録なし"
#: static/js/jumpserver.js:662
msgid "Unknown error occur" msgid "Unknown error occur"
msgstr "不明なエラーが発生" msgstr "不明なエラーが発生"
#: static/js/jumpserver.js:915 #: static/js/jumpserver.js:899
msgid "Password minimum length {N} bits" msgid "Password minimum length {N} bits"
msgstr "最小パスワード長 {N} ビット" msgstr "最小パスワード長 {N} ビット"
#: static/js/jumpserver.js:916 #: static/js/jumpserver.js:900
msgid "Must contain capital letters" msgid "Must contain capital letters"
msgstr "大文字を含める必要があります" msgstr "大文字を含める必要があります"
#: static/js/jumpserver.js:917 #: static/js/jumpserver.js:901
msgid "Must contain lowercase letters" msgid "Must contain lowercase letters"
msgstr "小文字を含める必要があります" msgstr "小文字を含める必要があります"
#: static/js/jumpserver.js:918 #: static/js/jumpserver.js:902
msgid "Must contain numeric characters" msgid "Must contain numeric characters"
msgstr "数字を含める必要があります。" msgstr "数字を含める必要があります。"
#: static/js/jumpserver.js:919 #: static/js/jumpserver.js:903
msgid "Must contain special characters" msgid "Must contain special characters"
msgstr "特殊文字を含める必要があります" msgstr "特殊文字を含める必要があります"
#: static/js/jumpserver.js:1098 static/js/jumpserver.js:1122 #~ msgid "Loading"
msgid "Export failed" #~ msgstr "読み込み中"
msgstr "エクスポートに失敗しました"
#: static/js/jumpserver.js:1139 #~ msgid "Search"
msgid "Import Success" #~ msgstr "検索"
msgstr "インポートの成功"
#: static/js/jumpserver.js:1144 #, javascript-format
msgid "Update Success" #~ msgid "Selected item %d"
msgstr "更新の成功" #~ msgstr "選択したアイテム % d"
#: static/js/jumpserver.js:1145 #~ msgid "Per page _MENU_"
msgid "Count" #~ msgstr "各ページ _MENU_"
msgstr "カウント"
#: static/js/jumpserver.js:1174 #~ msgid ""
msgid "Import failed" #~ "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "インポートに失敗しました" #~ msgstr ""
#~ "アイテムの結果を表示します _START_ に着く _END_; 合計 _TOTAL_ エントリ"
#: static/js/jumpserver.js:1179 #~ msgid "No match"
msgid "Update failed" #~ msgstr "一致しません"
msgstr "更新に失敗しました"
#~ msgid "No record"
#~ msgstr "記録なし"
#~ msgid "Export failed"
#~ msgstr "エクスポートに失敗しました"
#~ msgid "Import Success"
#~ msgstr "インポートの成功"
#~ msgid "Update Success"
#~ msgstr "更新の成功"
#~ msgid "Count"
#~ msgstr "カウント"
#~ msgid "Import failed"
#~ msgstr "インポートに失敗しました"
#~ msgid "Update failed"
#~ msgstr "更新に失敗しました"

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:144d439f8f3c96d00b1744de34b8a2a22b891f88ccb4b3c9669ad7273ecd08be oid sha256:4517c6a7464c68f949912b97c8a9abcc766ca19e32267a1d1da3f0e012471c1a
size 145525 size 146255

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:004adc99e5b3b3d42c1f1872d847d671f6bac2a3d6dd08148253c12c625c40de oid sha256:3f88466c49c30dea2bf52cfbb04c902473d9b5cba77174944a0ec2a565e7efaf
size 2741 size 1928

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-24 11:05+0800\n" "POT-Creation-Date: 2024-07-10 18:22+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -16,58 +16,62 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: static/js/jumpserver.js:259 #: static/js/jumpserver.js:264
msgid "Update is successful!" msgid "Update is successful!"
msgstr "更新成功" msgstr "更新成功"
#: static/js/jumpserver.js:261 #: static/js/jumpserver.js:266
msgid "An unknown error occurred while updating.." msgid "An unknown error occurred while updating.."
msgstr "更新时发生未知错误" msgstr "更新时发生未知错误"
#: static/js/jumpserver.js:324 static/js/jumpserver.js:362 #: static/js/jumpserver.js:339
#: static/js/jumpserver.js:364 msgid "Not found"
msgstr ""
#: static/js/jumpserver.js:341
msgid "Server error"
msgstr ""
#: static/js/jumpserver.js:343 static/js/jumpserver.js:381
#: static/js/jumpserver.js:383
msgid "Error" msgid "Error"
msgstr "错误" msgstr "错误"
#: static/js/jumpserver.js:324 #: static/js/jumpserver.js:349 static/js/jumpserver.js:390
msgid "Being used by the asset, please unbind the asset first."
msgstr "正在被资产使用中,请先解除资产绑定"
#: static/js/jumpserver.js:330 static/js/jumpserver.js:371
msgid "Delete the success" msgid "Delete the success"
msgstr "删除成功" msgstr "删除成功"
#: static/js/jumpserver.js:337 #: static/js/jumpserver.js:356
msgid "Are you sure about deleting it?" msgid "Are you sure about deleting it?"
msgstr "你确定删除吗 ?" msgstr "你确定删除吗 ?"
#: static/js/jumpserver.js:341 static/js/jumpserver.js:382 #: static/js/jumpserver.js:360 static/js/jumpserver.js:401
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: static/js/jumpserver.js:343 static/js/jumpserver.js:384 #: static/js/jumpserver.js:362 static/js/jumpserver.js:403
msgid "Confirm" msgid "Confirm"
msgstr "确认" msgstr "确认"
#: static/js/jumpserver.js:362 #: static/js/jumpserver.js:381
msgid "" msgid ""
"The organization contains undeleted information. Please try again after " "The organization contains undeleted information. Please try again after "
"deleting" "deleting"
msgstr "组织中包含未删除信息,请删除后重试" msgstr "组织中包含未删除信息,请删除后重试"
#: static/js/jumpserver.js:364 #: static/js/jumpserver.js:383
msgid "" msgid ""
"Do not perform this operation under this organization. Try again after " "Do not perform this operation under this organization. Try again after "
"switching to another organization" "switching to another organization"
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试" msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
#: static/js/jumpserver.js:378 #: static/js/jumpserver.js:397
msgid "" msgid ""
"Please ensure that the following information in the organization has been " "Please ensure that the following information in the organization has been "
"deleted" "deleted"
msgstr "请确保组织内的以下信息已删除" msgstr "请确保组织内的以下信息已删除"
#: static/js/jumpserver.js:379 #: static/js/jumpserver.js:398
msgid "" msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、" "User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission" "Labels、Asset permission"
@@ -75,84 +79,70 @@ msgstr ""
"用户列表、用户组、资产列表、网域列表、特权用户、系统用户、标签管理、资产授权" "用户列表、用户组、资产列表、网域列表、特权用户、系统用户、标签管理、资产授权"
"规则" "规则"
#: static/js/jumpserver.js:416 #: static/js/jumpserver.js:647
msgid "Loading"
msgstr "加载中"
#: static/js/jumpserver.js:417
msgid "Search"
msgstr "搜索"
#: static/js/jumpserver.js:420
#, javascript-format
msgid "Selected item %d"
msgstr "选中 %d 项"
#: static/js/jumpserver.js:424
msgid "Per page _MENU_"
msgstr "每页 _MENU_"
#: static/js/jumpserver.js:425
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/jumpserver.js:428
msgid "No match"
msgstr "没有匹配项"
#: static/js/jumpserver.js:429
msgid "No record"
msgstr "没有记录"
#: static/js/jumpserver.js:582
msgid "Unknown error occur" msgid "Unknown error occur"
msgstr "出现未知错误" msgstr "出现未知错误"
#: static/js/jumpserver.js:838 #: static/js/jumpserver.js:899
msgid "Password minimum length {N} bits" msgid "Password minimum length {N} bits"
msgstr "密码最小长度 {N} 位" msgstr "密码最小长度 {N} 位"
#: static/js/jumpserver.js:839 #: static/js/jumpserver.js:900
msgid "Must contain capital letters" msgid "Must contain capital letters"
msgstr "必须包含大写字母" msgstr "必须包含大写字母"
#: static/js/jumpserver.js:840 #: static/js/jumpserver.js:901
msgid "Must contain lowercase letters" msgid "Must contain lowercase letters"
msgstr "必须包含小写字母" msgstr "必须包含小写字母"
#: static/js/jumpserver.js:841 #: static/js/jumpserver.js:902
msgid "Must contain numeric characters" msgid "Must contain numeric characters"
msgstr "必须包含数字字符" msgstr "必须包含数字字符"
#: static/js/jumpserver.js:842 #: static/js/jumpserver.js:903
msgid "Must contain special characters" msgid "Must contain special characters"
msgstr "必须包含特殊字符" msgstr "必须包含特殊字符"
#: static/js/jumpserver.js:984 #~ msgid "Being used by the asset, please unbind the asset first."
msgid "Export failed" #~ msgstr "正在被资产使用中,请先解除资产绑定"
msgstr "导出失败"
#: static/js/jumpserver.js:1001 #~ msgid "Loading"
msgid "Import Success" #~ msgstr "加载中"
msgstr "导入成功"
#: static/js/jumpserver.js:1006 #~ msgid "Search"
msgid "Update Success" #~ msgstr "搜索"
msgstr "更新成功"
#: static/js/jumpserver.js:1007 #, javascript-format
msgid "Count" #~ msgid "Selected item %d"
msgstr "数量" #~ msgstr "选中 %d 项"
#: static/js/jumpserver.js:1035 #~ msgid "Per page _MENU_"
msgid "Import failed" #~ msgstr "每页 _MENU_"
msgstr "导入失败"
#: static/js/jumpserver.js:1040 #~ msgid ""
msgid "Update failed" #~ "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "更新失败" #~ msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/plugins/moment/moment.min.js:6 #~ msgid "No match"
msgid "\n" #~ msgstr "没有匹配项"
msgstr ""
#~ msgid "No record"
#~ msgstr "没有记录"
#~ msgid "Export failed"
#~ msgstr "导出失败"
#~ msgid "Import Success"
#~ msgstr "导入成功"
#~ msgid "Update Success"
#~ msgstr "更新成功"
#~ msgid "Count"
#~ msgstr "数量"
#~ msgid "Import failed"
#~ msgstr "导入失败"
#~ msgid "Update failed"
#~ msgstr "更新失败"

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:17da592df8b280d501a3b579c6a249b080bf07fbee34520a2012d8936d96ca14 oid sha256:7d4d2709e597e055072474f08be2f363d43df239240051024a77213ba48ecfac
size 145636 size 146366

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cc91c07e525f289e4277ec70a51438c10b694a4111f922bd4ee4b20f6e0f0cd0 oid sha256:9dfbc2f7eec8bd3fe6cff35456eb5b1af743206d1962dff1870f348347ef8e54
size 2832 size 2019

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-24 11:05+0800\n" "POT-Creation-Date: 2024-07-10 18:22+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -15,60 +15,65 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-ZhConverter: 繁化姬 dict-74c8d060-r1048 @ 2024/04/07 18:35:03 | https://zhconvert.org\n" "X-ZhConverter: 繁化姬 dict-74c8d060-r1048 @ 2024/04/07 18:35:03 | https://"
"zhconvert.org\n"
#: static/js/jumpserver.js:259 #: static/js/jumpserver.js:264
msgid "Update is successful!" msgid "Update is successful!"
msgstr "更新成功" msgstr "更新成功"
#: static/js/jumpserver.js:261 #: static/js/jumpserver.js:266
msgid "An unknown error occurred while updating.." msgid "An unknown error occurred while updating.."
msgstr "更新時發生未知錯誤" msgstr "更新時發生未知錯誤"
#: static/js/jumpserver.js:324 static/js/jumpserver.js:362 #: static/js/jumpserver.js:339
#: static/js/jumpserver.js:364 msgid "Not found"
msgstr ""
#: static/js/jumpserver.js:341
msgid "Server error"
msgstr ""
#: static/js/jumpserver.js:343 static/js/jumpserver.js:381
#: static/js/jumpserver.js:383
msgid "Error" msgid "Error"
msgstr "錯誤" msgstr "錯誤"
#: static/js/jumpserver.js:324 #: static/js/jumpserver.js:349 static/js/jumpserver.js:390
msgid "Being used by the asset, please unbind the asset first."
msgstr "正在被資產使用中,請先解除資產綁定"
#: static/js/jumpserver.js:330 static/js/jumpserver.js:371
msgid "Delete the success" msgid "Delete the success"
msgstr "刪除成功" msgstr "刪除成功"
#: static/js/jumpserver.js:337 #: static/js/jumpserver.js:356
msgid "Are you sure about deleting it?" msgid "Are you sure about deleting it?"
msgstr "你確定刪除嗎 ?" msgstr "你確定刪除嗎 ?"
#: static/js/jumpserver.js:341 static/js/jumpserver.js:382 #: static/js/jumpserver.js:360 static/js/jumpserver.js:401
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: static/js/jumpserver.js:343 static/js/jumpserver.js:384 #: static/js/jumpserver.js:362 static/js/jumpserver.js:403
msgid "Confirm" msgid "Confirm"
msgstr "確認" msgstr "確認"
#: static/js/jumpserver.js:362 #: static/js/jumpserver.js:381
msgid "" msgid ""
"The organization contains undeleted information. Please try again after " "The organization contains undeleted information. Please try again after "
"deleting" "deleting"
msgstr "組織中包含未刪除資訊,請刪除後重試" msgstr "組織中包含未刪除資訊,請刪除後重試"
#: static/js/jumpserver.js:364 #: static/js/jumpserver.js:383
msgid "" msgid ""
"Do not perform this operation under this organization. Try again after " "Do not perform this operation under this organization. Try again after "
"switching to another organization" "switching to another organization"
msgstr "請勿在此組織下執行此操作,切換到其他組織後重試" msgstr "請勿在此組織下執行此操作,切換到其他組織後重試"
#: static/js/jumpserver.js:378 #: static/js/jumpserver.js:397
msgid "" msgid ""
"Please ensure that the following information in the organization has been " "Please ensure that the following information in the organization has been "
"deleted" "deleted"
msgstr "請確保組織內的以下資訊已刪除" msgstr "請確保組織內的以下資訊已刪除"
#: static/js/jumpserver.js:379 #: static/js/jumpserver.js:398
msgid "" msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、" "User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission" "Labels、Asset permission"
@@ -76,84 +81,70 @@ msgstr ""
"用戶列表、用戶組、資產列表、網域列表、特權用戶、系統用戶、標籤管理、資產授權" "用戶列表、用戶組、資產列表、網域列表、特權用戶、系統用戶、標籤管理、資產授權"
"規則" "規則"
#: static/js/jumpserver.js:416 #: static/js/jumpserver.js:647
msgid "Loading"
msgstr "載入中"
#: static/js/jumpserver.js:417
msgid "Search"
msgstr "搜索"
#: static/js/jumpserver.js:420
#, javascript-format
msgid "Selected item %d"
msgstr "選中 %d 項"
#: static/js/jumpserver.js:424
msgid "Per page _MENU_"
msgstr "每頁 _MENU_"
#: static/js/jumpserver.js:425
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "顯示第 _START_ 至 _END_ 項結果; 總共 _TOTAL_ 項"
#: static/js/jumpserver.js:428
msgid "No match"
msgstr "沒有匹配項"
#: static/js/jumpserver.js:429
msgid "No record"
msgstr "沒有記錄"
#: static/js/jumpserver.js:582
msgid "Unknown error occur" msgid "Unknown error occur"
msgstr "出現未知錯誤" msgstr "出現未知錯誤"
#: static/js/jumpserver.js:838 #: static/js/jumpserver.js:899
msgid "Password minimum length {N} bits" msgid "Password minimum length {N} bits"
msgstr "密碼最小長度 {N} 位" msgstr "密碼最小長度 {N} 位"
#: static/js/jumpserver.js:839 #: static/js/jumpserver.js:900
msgid "Must contain capital letters" msgid "Must contain capital letters"
msgstr "必須包含大寫字母" msgstr "必須包含大寫字母"
#: static/js/jumpserver.js:840 #: static/js/jumpserver.js:901
msgid "Must contain lowercase letters" msgid "Must contain lowercase letters"
msgstr "必須包含小寫字母" msgstr "必須包含小寫字母"
#: static/js/jumpserver.js:841 #: static/js/jumpserver.js:902
msgid "Must contain numeric characters" msgid "Must contain numeric characters"
msgstr "必須包含數字字元" msgstr "必須包含數字字元"
#: static/js/jumpserver.js:842 #: static/js/jumpserver.js:903
msgid "Must contain special characters" msgid "Must contain special characters"
msgstr "必須包含特殊字元" msgstr "必須包含特殊字元"
#: static/js/jumpserver.js:984 #~ msgid "Being used by the asset, please unbind the asset first."
msgid "Export failed" #~ msgstr "正在被資產使用中,請先解除資產綁定"
msgstr "匯出失敗"
#: static/js/jumpserver.js:1001 #~ msgid "Loading"
msgid "Import Success" #~ msgstr "載入中"
msgstr "匯入成功"
#: static/js/jumpserver.js:1006 #~ msgid "Search"
msgid "Update Success" #~ msgstr "搜索"
msgstr "更新成功"
#: static/js/jumpserver.js:1007 #, javascript-format
msgid "Count" #~ msgid "Selected item %d"
msgstr "數量" #~ msgstr "選中 %d 項"
#: static/js/jumpserver.js:1035 #~ msgid "Per page _MENU_"
msgid "Import failed" #~ msgstr "每頁 _MENU_"
msgstr "匯入失敗"
#: static/js/jumpserver.js:1040 #~ msgid ""
msgid "Update failed" #~ "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "更新失敗" #~ msgstr "顯示第 _START_ 至 _END_ 項結果; 總共 _TOTAL_ 項"
#: static/js/plugins/moment/moment.min.js:6 #~ msgid "No match"
msgid "\n" #~ msgstr "沒有匹配項"
msgstr ""
#~ msgid "No record"
#~ msgstr "沒有記錄"
#~ msgid "Export failed"
#~ msgstr "匯出失敗"
#~ msgid "Import Success"
#~ msgstr "匯入成功"
#~ msgid "Update Success"
#~ msgstr "更新成功"
#~ msgid "Count"
#~ msgstr "數量"
#~ msgid "Import failed"
#~ msgstr "匯入失敗"
#~ msgid "Update failed"
#~ msgstr "更新失敗"

View File

@@ -14,7 +14,8 @@ class SiteMessageUtil:
def send_msg(cls, subject, message, user_ids=(), group_ids=(), def send_msg(cls, subject, message, user_ids=(), group_ids=(),
sender=None, is_broadcast=False): sender=None, is_broadcast=False):
if not any((user_ids, group_ids, is_broadcast)): if not any((user_ids, group_ids, is_broadcast)):
raise ValueError('No recipient is specified') logger.warning(f'No recipient is specified, message subject: {subject}')
return
with transaction.atomic(): with transaction.atomic():
site_msg = SiteMessageModel.objects.create( site_msg = SiteMessageModel.objects.create(

View File

@@ -39,9 +39,10 @@ class AdHocRunner:
def check_module(self): def check_module(self):
if self.module not in self.cmd_modules_choices: if self.module not in self.cmd_modules_choices:
return return
if self.module_args and self.module_args.split()[0] in settings.SECURITY_COMMAND_BLACKLIST: command = self.module_args
if command and set(command.split()).intersection(set(settings.SECURITY_COMMAND_BLACKLIST)):
raise CommandInBlackListException( raise CommandInBlackListException(
"Command is rejected by black list: {}".format(self.module_args.split()[0])) "Command is rejected by black list: {}".format(self.module_args))
def set_local_connection(self): def set_local_connection(self):
if self.job_module in self.need_local_connection_modules_choices: if self.job_module in self.need_local_connection_modules_choices:

View File

@@ -478,6 +478,16 @@ class JobExecution(JMSOrgBaseModel):
for acl in acls: for acl in acls:
if self.match_command_group(acl, asset): if self.match_command_group(acl, asset):
break break
command = self.current_job.args
if command and set(command.split()).intersection(set(settings.SECURITY_COMMAND_BLACKLIST)):
CommandExecutionAlert({
"assets": self.current_job.assets.all(),
"input": self.material,
"risk_level": RiskLevelChoices.reject,
"user": self.creator,
}).publish_async()
raise CommandInBlackListException(
"Command is rejected by black list: {}".format(self.current_job.args))
def check_danger_keywords(self): def check_danger_keywords(self):
lines = self.job.playbook.check_dangerous_keywords() lines = self.job.playbook.check_dangerous_keywords()

View File

@@ -60,7 +60,7 @@ def check_registered_tasks(*args, **kwargs):
'perms.tasks.check_asset_permission_will_expired', 'perms.tasks.check_asset_permission_will_expired',
'ops.tasks.create_or_update_registered_periodic_tasks', 'perms.tasks.check_asset_permission_expired', 'ops.tasks.create_or_update_registered_periodic_tasks', 'perms.tasks.check_asset_permission_expired',
'settings.tasks.ldap.import_ldap_user_periodic', 'users.tasks.check_password_expired_periodic', 'settings.tasks.ldap.import_ldap_user_periodic', 'users.tasks.check_password_expired_periodic',
'common.utils.verify_code.send_async', 'assets.tasks.nodes_amount.check_node_assets_amount_period_task', 'common.utils.verify_code.send_sms_async', 'assets.tasks.nodes_amount.check_node_assets_amount_period_task',
'users.tasks.check_user_expired', 'orgs.tasks.refresh_org_cache_task', 'users.tasks.check_user_expired', 'orgs.tasks.refresh_org_cache_task',
'terminal.tasks.upload_session_replay_to_external_storage', 'terminal.tasks.clean_orphan_session', 'terminal.tasks.upload_session_replay_to_external_storage', 'terminal.tasks.clean_orphan_session',
'audits.tasks.clean_audits_log_period', 'authentication.tasks.clean_django_sessions' 'audits.tasks.clean_audits_log_period', 'authentication.tasks.clean_django_sessions'

View File

@@ -34,6 +34,18 @@ def job_task_activity_callback(self, job_id, *args, **kwargs):
return resource_ids, org_id return resource_ids, org_id
def _run_ops_job_execution(execution):
try:
with tmp_to_org(execution.org):
execution.start()
except SoftTimeLimitExceeded:
execution.set_error('Run timeout')
logger.error("Run adhoc timeout")
except Exception as e:
execution.set_error(e)
logger.error("Start adhoc execution error: {}".format(e))
@shared_task( @shared_task(
soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task"), soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task"),
activity_callback=job_task_activity_callback activity_callback=job_task_activity_callback
@@ -48,15 +60,7 @@ def run_ops_job(job_id):
with tmp_to_org(job.org): with tmp_to_org(job.org):
execution = job.create_execution() execution = job.create_execution()
execution.creator = job.creator execution.creator = job.creator
run_ops_job_execution(execution.id) _run_ops_job_execution(execution)
try:
execution.start()
except SoftTimeLimitExceeded:
execution.set_error('Run timeout')
logger.error("Run adhoc timeout")
except Exception as e:
execution.set_error(e)
logger.error("Start adhoc execution error: {}".format(e))
def job_execution_task_activity_callback(self, execution_id, *args, **kwargs): def job_execution_task_activity_callback(self, execution_id, *args, **kwargs):
@@ -79,16 +83,7 @@ def run_ops_job_execution(execution_id, **kwargs):
if not execution: if not execution:
logger.error("Did not get the execution: {}".format(execution_id)) logger.error("Did not get the execution: {}".format(execution_id))
return return
_run_ops_job_execution(execution)
try:
with tmp_to_org(execution.org):
execution.start()
except SoftTimeLimitExceeded:
execution.set_error('Run timeout')
logger.error("Run adhoc timeout")
except Exception as e:
execution.set_error(e)
logger.error("Start adhoc execution error: {}".format(e))
@shared_task(verbose_name=_('Clear celery periodic tasks')) @shared_task(verbose_name=_('Clear celery periodic tasks'))

View File

@@ -6,7 +6,6 @@ from rest_framework import serializers
from accounts.const import Source from accounts.const import Source
from accounts.models import AccountTemplate, Account from accounts.models import AccountTemplate, Account
from accounts.tasks import push_accounts_to_assets_task
from assets.models import Asset, Node from assets.models import Asset, Node
from common.serializers import ResourceLabelsMixin from common.serializers import ResourceLabelsMixin
from common.serializers.fields import BitChoicesField, ObjectRelatedField from common.serializers.fields import BitChoicesField, ObjectRelatedField
@@ -79,44 +78,35 @@ class AssetPermissionSerializer(ResourceLabelsMixin, BulkOrgResourceModelSeriali
return Asset.objects.filter(id__in=asset_ids) return Asset.objects.filter(id__in=asset_ids)
def create_accounts(self, assets): def create_accounts(self, assets):
need_create_accounts = [] account_objs = []
account_attribute = [ account_attribute = [
'name', 'username', 'secret_type', 'secret', 'name', 'username', 'secret_type', 'secret',
'privileged', 'is_active', 'org_id' 'privileged', 'is_active', 'org_id'
] ]
for asset in assets: for asset in assets:
asset_exist_accounts = Account.objects.none() asset_exist_account_names = set(asset.accounts.values_list('name', flat=True))
asset_exist_account_names = asset.accounts.values_list('name', flat=True)
asset_exist_accounts = asset.accounts.values('username', 'secret_type')
username_secret_type_set = {(acc['username'], acc['secret_type']) for acc in asset_exist_accounts}
for template in self.template_accounts: for template in self.template_accounts:
asset_exist_accounts |= asset.accounts.filter( condition = (template.username, template.secret_type)
username=template.username, if condition in username_secret_type_set or template.name in asset_exist_account_names:
secret_type=template.secret_type,
)
username_secret_type_dict = asset_exist_accounts.values('username', 'secret_type')
for template in self.template_accounts:
condition = {
'username': template.username,
'secret_type': template.secret_type
}
if condition in username_secret_type_dict or \
template.name in asset_exist_account_names:
continue continue
account_data = {key: getattr(template, key) for key in account_attribute} account_data = {key: getattr(template, key) for key in account_attribute}
account_data['su_from'] = template.get_su_from_account(asset) account_data['su_from'] = template.get_su_from_account(asset)
account_data['source'] = Source.TEMPLATE account_data['source'] = Source.TEMPLATE
account_data['source_id'] = str(template.id) account_data['source_id'] = str(template.id)
need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data})) account_objs.append(Account(asset=asset, **account_data))
return Account.objects.bulk_create(need_create_accounts)
def create_and_push_account(self, nodes, assets): if account_objs:
Account.objects.bulk_create(account_objs)
def create_account_through_template(self, nodes, assets):
if not self.template_accounts: if not self.template_accounts:
return return
assets = self.get_all_assets(nodes, assets) assets = self.get_all_assets(nodes, assets)
accounts = self.create_accounts(assets) self.create_accounts(assets)
account_ids = [str(account.id) for account in accounts]
slice_count = 20
for i in range(0, len(account_ids), slice_count):
push_accounts_to_assets_task.delay(account_ids[i:i + slice_count])
def validate_accounts(self, usernames): def validate_accounts(self, usernames):
template_ids = [] template_ids = []
@@ -164,7 +154,7 @@ class AssetPermissionSerializer(ResourceLabelsMixin, BulkOrgResourceModelSeriali
instance.nodes.add(*nodes_to_set) instance.nodes.add(*nodes_to_set)
def validate(self, attrs): def validate(self, attrs):
self.create_and_push_account( self.create_account_through_template(
attrs.get("nodes", []), attrs.get("nodes", []),
attrs.get("assets", []) attrs.get("assets", [])
) )

View File

@@ -15,8 +15,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
<br/> <br>
-
<p> <p>
{% trans 'If you have any question, please contact the administrator' %} {% trans 'If you have any question, please contact the administrator' %}
</p> </p>

View File

@@ -69,7 +69,9 @@ class VaultSettingSerializer(serializers.Serializer):
class ChatAISettingSerializer(serializers.Serializer): class ChatAISettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('Chat AI') PREFIX_TITLE = _('Chat AI')
GPT_MODEL_CHOICES = [] API_MODEL = Protocol.gpt_protocols()[Protocol.chatgpt]['setting']['api_mode']
GPT_MODEL_CHOICES = API_MODEL['choices']
GPT_MODEL_DEFAULT = API_MODEL['default']
CHAT_AI_ENABLED = serializers.BooleanField( CHAT_AI_ENABLED = serializers.BooleanField(
required=False, label=_('Enable Chat AI') required=False, label=_('Enable Chat AI')
@@ -84,26 +86,9 @@ class ChatAISettingSerializer(serializers.Serializer):
allow_blank=True, required=False, label=_('Proxy') allow_blank=True, required=False, label=_('Proxy')
) )
GPT_MODEL = serializers.ChoiceField( GPT_MODEL = serializers.ChoiceField(
default='', choices=GPT_MODEL_CHOICES, label=_("GPT Model"), required=False, default=GPT_MODEL_DEFAULT, choices=GPT_MODEL_CHOICES, label=_("GPT Model"), required=False,
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_GPT_MODEL_choices()
def set_GPT_MODEL_choices(self):
field_gpt_model = self.fields.get("GPT_MODEL")
if not field_gpt_model:
return
gpt_api_model = Protocol.gpt_protocols()[Protocol.chatgpt]['setting']['api_mode']
choices = gpt_api_model['choices']
field_gpt_model.choices = choices
field_gpt_model.default = gpt_api_model['default']
cls = self.__class__
if cls.GPT_MODEL_CHOICES:
return
cls.GPT_MODEL_CHOICES.extend(choices)
class TicketSettingSerializer(serializers.Serializer): class TicketSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('Ticket') PREFIX_TITLE = _('Ticket')

View File

@@ -30,7 +30,7 @@ class EmailSettingSerializer(serializers.Serializer):
) )
EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("Host")) EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("Host"))
EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("Port")) EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("Port"))
EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("Account")) EMAIL_HOST_USER = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_("Account"))
EMAIL_HOST_PASSWORD = EncryptedField( EMAIL_HOST_PASSWORD = EncryptedField(
max_length=1024, required=False, label=_("Password"), max_length=1024, required=False, label=_("Password"),
help_text=_("Tips: Some provider use token except password") help_text=_("Tips: Some provider use token except password")

View File

@@ -14,6 +14,10 @@ from .auth import (
) )
from .basic import BasicSettingSerializer from .basic import BasicSettingSerializer
from .cleaning import CleaningSerializer from .cleaning import CleaningSerializer
from .feature import (
AnnouncementSettingSerializer, OpsSettingSerializer, VaultSettingSerializer,
TicketSettingSerializer, ChatAISettingSerializer, VirtualAppSerializer
)
from .msg import EmailSettingSerializer, EmailContentSettingSerializer from .msg import EmailSettingSerializer, EmailContentSettingSerializer
from .other import OtherSettingSerializer from .other import OtherSettingSerializer
from .security import SecuritySettingSerializer from .security import SecuritySettingSerializer
@@ -90,7 +94,13 @@ class SettingsSerializer(
TencentSMSSettingSerializer, TencentSMSSettingSerializer,
CMPP2SMSSettingSerializer, CMPP2SMSSettingSerializer,
CustomSMSSettingSerializer, CustomSMSSettingSerializer,
PasskeySettingSerializer PasskeySettingSerializer,
ChatAISettingSerializer,
AnnouncementSettingSerializer,
OpsSettingSerializer,
VaultSettingSerializer,
TicketSettingSerializer,
VirtualAppSerializer,
): ):
PREFIX_TITLE = _('Setting') PREFIX_TITLE = _('Setting')
CACHE_KEY = 'SETTING_FIELDS_MAPPING' CACHE_KEY = 'SETTING_FIELDS_MAPPING'

View File

@@ -1,3 +1,3 @@
{% if not USE_XPACK %} {% if not XPACK_ENABLED %}
<strong>Copyright</strong> {{ COPYRIGHT }} <strong>Copyright</strong> {{ COPYRIGHT }}
{% endif %} {% endif %}

View File

@@ -102,6 +102,8 @@ class AppletMethod:
methods = defaultdict(list) methods = defaultdict(list)
has_applet_hosts = AppletHost.objects.filter(is_active=True).exists() has_applet_hosts = AppletHost.objects.filter(is_active=True).exists()
if not has_applet_hosts:
return methods
applets = Applet.objects.filter(is_active=True) applets = Applet.objects.filter(is_active=True)
for applet in applets: for applet in applets:
for protocol in applet.protocols: for protocol in applet.protocols:
@@ -110,7 +112,7 @@ class AppletMethod:
'label': applet.display_name, 'label': applet.display_name,
'type': 'applet', 'type': 'applet',
'icon': applet.icon, 'icon': applet.icon,
'disabled': not applet.is_active or not has_applet_hosts, 'disabled': not applet.is_active,
}) })
return methods return methods

View File

@@ -70,6 +70,7 @@ class Migration(migrations.Migration):
to='terminal.applethost', verbose_name='Host')), to='terminal.applethost', verbose_name='Host')),
], ],
options={ options={
'verbose_name': 'Applet publication',
'unique_together': {('applet', 'host')}, 'unique_together': {('applet', 'host')},
}, },
), ),
@@ -91,6 +92,7 @@ class Migration(migrations.Migration):
verbose_name='Hosting')), verbose_name='Hosting')),
], ],
options={ options={
'verbose_name': 'Applet host deployment',
'ordering': ('-date_start',), 'ordering': ('-date_start',),
}, },
), ),
@@ -115,11 +117,13 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='appletpublication', model_name='appletpublication',
name='applet', name='applet',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applet', verbose_name='Applet'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications',
to='terminal.applet', verbose_name='Applet'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='appletpublication', model_name='appletpublication',
name='host', name='host',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applethost', verbose_name='Host'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications',
to='terminal.applethost', verbose_name='Host'),
), ),
] ]

View File

@@ -15,6 +15,7 @@ from assets.models import Platform
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
from common.utils import lazyproperty, get_logger from common.utils import lazyproperty, get_logger
from common.utils.yml import yaml_load_with_i18n from common.utils.yml import yaml_load_with_i18n
from terminal.const import PublishStatus
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -171,12 +172,12 @@ class Applet(JMSBaseModel):
if not hosts: if not hosts:
return None return None
spec_label = asset.labels.filter(label__name__in=['AppletHost', '发布机']).first() spec_label_values = asset.get_labels().filter(
if spec_label and spec_label.label: name__in=['AppletHost', '发布机']
label_value = spec_label.label.value ).values_list('value', flat=True)
matched = [host for host in hosts if host.name == label_value] host_matched = [host for host in hosts if host.name in spec_label_values]
if matched: if host_matched:
return random.choice(matched) return random.choice(host_matched)
hosts = [h for h in hosts if h.auto_create_accounts] hosts = [h for h in hosts if h.auto_create_accounts]
prefer_key = self.host_prefer_key_tpl.format(user.id) prefer_key = self.host_prefer_key_tpl.format(user.id)
@@ -278,7 +279,9 @@ class Applet(JMSBaseModel):
if not host: if not host:
return None return None
logger.info('Select applet host: {}'.format(host.name)) logger.info('Select applet host: {}'.format(host.name))
if not self.is_available_on_host(host):
logger.debug('No available applet {} for applet host: {}'.format(self.name, host.name))
return None
valid_accounts = host.accounts.all().filter(is_active=True, privileged=False) valid_accounts = host.accounts.all().filter(is_active=True, privileged=False)
account = self.try_to_use_same_account(user, host) account = self.try_to_use_same_account(user, host)
if not account: if not account:
@@ -311,6 +314,14 @@ class Applet(JMSBaseModel):
platform.delete() platform.delete()
return super().delete(using, keep_parents) return super().delete(using, keep_parents)
def is_available_on_host(self, host):
publication = AppletPublication.objects.filter(applet=self, host=host).first()
if not publication:
return False
if publication.status in [PublishStatus.pending, PublishStatus.failed]:
return False
return True
class AppletPublication(JMSBaseModel): class AppletPublication(JMSBaseModel):
applet = models.ForeignKey('Applet', on_delete=models.CASCADE, related_name='publications', applet = models.ForeignKey('Applet', on_delete=models.CASCADE, related_name='publications',
@@ -322,3 +333,4 @@ class AppletPublication(JMSBaseModel):
class Meta: class Meta:
unique_together = ('applet', 'host') unique_together = ('applet', 'host')
verbose_name = _("Applet publication")

View File

@@ -145,6 +145,7 @@ class AppletHostDeployment(JMSBaseModel):
class Meta: class Meta:
ordering = ('-date_start',) ordering = ('-date_start',)
verbose_name = _("Applet host deployment")
def start(self, **kwargs): def start(self, **kwargs):
# 重新初始化部署applet host 关联的终端需要删除 # 重新初始化部署applet host 关联的终端需要删除

View File

@@ -58,6 +58,16 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
'terminal_display': {'label': _('Terminal display')}, 'terminal_display': {'label': _('Terminal display')},
} }
def get_fields(self):
fields = super().get_fields()
self.pop_fields_if_need(fields)
return fields
def pop_fields_if_need(self, fields):
request = self.context.get('request')
if request and request.method != 'GET':
fields.pop("command_amount", None)
def validate_asset(self, value): def validate_asset(self, value):
max_length = self.Meta.model.asset.field.max_length max_length = self.Meta.model.asset.field.max_length
value = pretty_string(value, max_length=max_length) value = pretty_string(value, max_length=max_length)

View File

@@ -252,6 +252,16 @@ class BaseStorageSerializer(serializers.ModelSerializer):
serializer = serializer_class serializer = serializer_class
return serializer return serializer
def to_representation(self, instance):
data = super().to_representation(instance)
need_translate_comments = {
'Store locally': _('Store locally'),
'Do not save': _('Do not save')
}
comment = instance.comment
data['comment'] = need_translate_comments.get(comment, comment)
return data
def save(self, **kwargs): def save(self, **kwargs):
instance = super().save(**kwargs) instance = super().save(**kwargs)
if self.validated_data.get('is_default', False): if self.validated_data.get('is_default', False):

View File

@@ -14,7 +14,8 @@ from accounts.const import AliasAccount
from common.db.encoder import ModelJSONFieldEncoder from common.db.encoder import ModelJSONFieldEncoder
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
from common.exceptions import JMSException from common.exceptions import JMSException
from common.utils import reverse from common.utils import reverse, get_logger
from common.utils.lock import DistributedLock
from common.utils.timezone import as_current_tz from common.utils.timezone import as_current_tz
from orgs.models import Organization from orgs.models import Organization
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
@@ -26,6 +27,8 @@ from tickets.errors import AlreadyClosed
from tickets.handlers import get_ticket_handler from tickets.handlers import get_ticket_handler
from ..flow import TicketFlow from ..flow import TicketFlow
logger = get_logger(__file__)
__all__ = [ __all__ = [
'Ticket', 'TicketStep', 'TicketAssignee', 'Ticket', 'TicketStep', 'TicketAssignee',
'SuperTicket', 'SubTicketManager' 'SuperTicket', 'SubTicketManager'
@@ -374,25 +377,28 @@ class Ticket(StatusMixin, JMSBaseModel):
date_created = as_current_tz(self.date_created) date_created = as_current_tz(self.date_created)
date_prefix = date_created.strftime('%Y%m%d') date_prefix = date_created.strftime('%Y%m%d')
ticket = Ticket.objects.all().select_for_update().filter( ticket = Ticket.objects.filter(
serial_num__startswith=date_prefix serial_num__startswith=date_prefix
).order_by('-date_created').first() ).order_by('-serial_num').first()
last_num = 0 last_num = 0
if ticket: if ticket:
last_num = ticket.serial_num[8:] last_num = ticket.serial_num[8:]
last_num = int(last_num) last_num = int(last_num)
num = '%04d' % (last_num + 1) num = '%04d' % (last_num + 1)
return '{}{}'.format(date_prefix, num) return f'{date_prefix}{num}'
def set_serial_num(self): def set_serial_num(self):
if self.serial_num: if self.serial_num:
return return
lock_key = 'TICKET_LOCK_SET_SERIAL_NUM'
with DistributedLock(lock_key):
try: try:
self.serial_num = self.get_next_serial_num() self.serial_num = self.get_next_serial_num()
self.save(update_fields=('serial_num',)) self.save(update_fields=('serial_num',))
except IntegrityError as e: except IntegrityError as e:
logger.error(f'Set ticket serial number error: {e}')
if e.args[0] == 1062: if e.args[0] == 1062:
# 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突 # 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突
# 但概率小,这里只报错,用户重新提交即可 # 但概率小,这里只报错,用户重新提交即可

View File

@@ -160,6 +160,22 @@ class AuthMixin:
return True return True
return False return False
def check_need_update_password(self):
if self.is_local and self.need_update_password:
return True
return False
@staticmethod
def check_passwd_too_simple(password):
simple_passwords = ['admin', 'ChangeMe']
if password in simple_passwords:
return True
return False
def is_auth_backend_model(self):
backend = getattr(self, 'backend', None)
return backend == settings.AUTH_BACKEND_MODEL
@staticmethod @staticmethod
def get_public_key_md5(key): def get_public_key_md5(key):
try: try:

View File

@@ -65,16 +65,15 @@ def user_authenticated_handle(user, created, source, attrs=None, **kwargs):
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def save_passwd_change(sender, instance: User, **kwargs): def save_passwd_change(sender, instance: User, **kwargs):
if instance.source != User.Source.local.value or not instance.password:
return
passwords = UserPasswordHistory.objects \ passwords = UserPasswordHistory.objects \
.filter(user=instance) \ .filter(user=instance) \
.order_by('-date_created') \ .order_by('-date_created') \
.values_list('password', flat=True) .values_list('password', flat=True)[:settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT]
passwords = passwords[:int(settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT)]
for p in passwords: if instance.password not in list(passwords):
if instance.password == p:
break
else:
UserPasswordHistory.objects.create( UserPasswordHistory.objects.create(
user=instance, password=instance.password, user=instance, password=instance.password,
date_created=instance.date_password_last_updated date_created=instance.date_password_last_updated

View File

@@ -9,9 +9,8 @@
<br /> <br />
<br /> <br />
<a href="{{ update_password_url }}">{% trans 'Click here update password' %}</a> <a href="{{ update_password_url }}">{% trans 'Click here update password' %}</a>
<br /> <br/>
</p> </p>
-
<p> <p>
{% trans 'If your password has expired, please click the link below to' %} {% trans 'If your password has expired, please click the link below to' %}
<a href="{{ forget_password_url }}?email={{ email }}">{% trans 'Reset password' %}</a> <a href="{{ forget_password_url }}?email={{ email }}">{% trans 'Reset password' %}</a>

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