mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-21 11:33:03 +00:00
Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d16879d26 | ||
|
|
4b981fd93c | ||
|
|
6720ecc6e0 | ||
|
|
b0f86e43a6 | ||
|
|
9b0c81333f | ||
|
|
05fc966444 | ||
|
|
b87650038f | ||
|
|
d4f69a7ff8 | ||
|
|
0e1e26c29c | ||
|
|
1b8cdbc4dd | ||
|
|
2a781c228f | ||
|
|
35d6b0f16a | ||
|
|
ca8987fef6 | ||
|
|
b385133071 | ||
|
|
aa78a03efa | ||
|
|
31f8a19392 | ||
|
|
7a528b499a | ||
|
|
1c6ce422cf | ||
|
|
f9cf2ea2e5 | ||
|
|
575b3a617f | ||
|
|
b7362d3f51 | ||
|
|
6ee3860124 | ||
|
|
7e111da529 | ||
|
|
578458f734 | ||
|
|
bd56697d6d | ||
|
|
aad824d127 | ||
|
|
63f828da0b | ||
|
|
7c211b3fb6 | ||
|
|
3881edd2ba | ||
|
|
b882b12d04 | ||
|
|
addd2e7d1c | ||
|
|
ad6d2e1cd7 | ||
|
|
5f07271afa | ||
|
|
efdcd4c708 | ||
|
|
b62763bca3 | ||
|
|
e95da730f2 | ||
|
|
43fa3f420a | ||
|
|
0311446384 | ||
|
|
f7030e4fee | ||
|
|
fce8cc375f | ||
|
|
920199c6df | ||
|
|
d09eb3c4fa | ||
|
|
6e8affcdd6 | ||
|
|
0b3a7bb020 | ||
|
|
647736f4e3 | ||
|
|
cbc09d84df | ||
|
|
4c957dd03b | ||
|
|
d34b65890f | ||
|
|
b53968ac00 | ||
|
|
f2ccb15101 | ||
|
|
db5bf046fc | ||
|
|
59c87483e6 | ||
|
|
26420b78f8 | ||
|
|
e47bdc093e | ||
|
|
3dde80a60a | ||
|
|
e373a79d63 | ||
|
|
744a5cd0e3 | ||
|
|
37ca4a46ee | ||
|
|
0dc9214f98 | ||
|
|
513508654b | ||
|
|
ef2b12fa0f | ||
|
|
4e719ecacd | ||
|
|
755a124b50 | ||
|
|
d6888776e7 | ||
|
|
29e233e715 | ||
|
|
99c3696d96 | ||
|
|
ed6de83e8c | ||
|
|
134f1a440c | ||
|
|
7da82242fe | ||
|
|
2fd50d2425 | ||
|
|
41a3e89248 | ||
|
|
b125297c37 | ||
|
|
24255b69ee | ||
|
|
3bb51b39c4 | ||
|
|
b54da7d3b3 | ||
|
|
534af0abf0 | ||
|
|
8b0073333b | ||
|
|
d8af2274f4 | ||
|
|
3dd828d703 | ||
|
|
fa6b4a5b63 | ||
|
|
8bd86c77f9 | ||
|
|
3828e89cf8 | ||
|
|
e531b040ef | ||
|
|
3eee84a34e | ||
|
|
ab29df5991 | ||
|
|
b042f00688 | ||
|
|
5beebaf51c | ||
|
|
50f075cc7e | ||
|
|
e997236159 | ||
|
|
c8b1d892e3 | ||
|
|
9cb9e7328b | ||
|
|
85129da942 | ||
|
|
1cb00b1db4 | ||
|
|
c3798bfa95 | ||
|
|
1d280599ae | ||
|
|
ee8d7cdcac | ||
|
|
1b4114fd5f | ||
|
|
3c6c476f2e | ||
|
|
f19e3fedbd | ||
|
|
542e64278f | ||
|
|
cd76294e81 | ||
|
|
4f9158b2ad | ||
|
|
e319f20296 | ||
|
|
b00f3a851c | ||
|
|
ab529fd22c | ||
|
|
c2784c44ad | ||
|
|
512e727ac6 | ||
|
|
2dd0154967 | ||
|
|
f55869a449 | ||
|
|
b6f3c23787 | ||
|
|
6982ab1efc | ||
|
|
db4d841bb0 | ||
|
|
ef91ebb468 | ||
|
|
6264319c51 | ||
|
|
1417abecfb | ||
|
|
bd548b3fe2 | ||
|
|
94cef9ea6e | ||
|
|
a338613b5a | ||
|
|
0d833a966c | ||
|
|
76b6489636 | ||
|
|
763fe778d5 | ||
|
|
cf1dc79c68 | ||
|
|
7973239424 | ||
|
|
1baacd0b2c | ||
|
|
054d385ffc | ||
|
|
50d3a4906a | ||
|
|
c8b7008d42 | ||
|
|
e94520a3fd | ||
|
|
55e8e34226 | ||
|
|
8755ece633 | ||
|
|
c545e2a3aa | ||
|
|
1068662ab1 | ||
|
|
75141741a1 | ||
|
|
9da507bb62 | ||
|
|
160293365a | ||
|
|
7a19007aba | ||
|
|
f866b93f96 | ||
|
|
b9e64747ac | ||
|
|
25a473dc99 | ||
|
|
e3bf015aa9 | ||
|
|
6d3d4a08af | ||
|
|
9554de4ea6 | ||
|
|
6157ff7b7d | ||
|
|
774fd176fd | ||
|
|
b489db8054 | ||
|
|
6b9fa6e01f | ||
|
|
9b59954393 | ||
|
|
ecaf19563f | ||
|
|
c431e96eaf | ||
|
|
d86f241450 | ||
|
|
3252db31fe | ||
|
|
dac118dd26 | ||
|
|
181eb621c0 | ||
|
|
828582333d | ||
|
|
657f7f822b | ||
|
|
93627e4f9d | ||
|
|
2adb2519fa | ||
|
|
56373e362b | ||
|
|
32ec48ac14 | ||
|
|
02fc045370 | ||
|
|
e4ac73896f | ||
|
|
1518f792d6 | ||
|
|
67277dd622 | ||
|
|
82e7f020ea | ||
|
|
f20b9e01ab | ||
|
|
8cf8a3701b | ||
|
|
7ba24293d1 | ||
|
|
f10114c9ed | ||
|
|
cf31cbfb07 | ||
|
|
0edad24d5d | ||
|
|
1f1c1a9157 | ||
|
|
6c9d271ae1 | ||
|
|
6ff852e225 | ||
|
|
baa75dc735 | ||
|
|
8a9f0436b8 | ||
|
|
a9620a3cbe | ||
|
|
769e7dc8a0 | ||
|
|
2a70449411 | ||
|
|
8df720f19e | ||
|
|
dabbb45f6e | ||
|
|
ce24c1c3fd | ||
|
|
3c54c82ce9 |
28
.github/workflows/build-base-image.yml
vendored
28
.github/workflows/build-base-image.yml
vendored
@@ -1,21 +1,33 @@
|
||||
name: Build and Push Base Image
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'pr*'
|
||||
- 'dev'
|
||||
- 'v*'
|
||||
paths:
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
- 'Dockerfile-base'
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- Dockerfile-base
|
||||
- package.json
|
||||
- go.mod
|
||||
- yarn.lock
|
||||
- pom.xml
|
||||
- install_deps.sh
|
||||
- utils/clean_site_packages.sh
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -55,6 +67,6 @@ jobs:
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git add Dockerfile
|
||||
git commit -m "perf: Update Dockerfile with new base image tag"
|
||||
git push
|
||||
git push origin ${{ github.event.pull_request.head.ref }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
31
.github/workflows/check-compilemessages.yml
vendored
Normal file
31
.github/workflows/check-compilemessages.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Check I18n files CompileMessages
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'dev'
|
||||
paths:
|
||||
- 'apps/i18n/core/**/*.po'
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
jobs:
|
||||
compile-messages-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and check compilemessages
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
file: Dockerfile
|
||||
target: stage-build
|
||||
tags: jumpserver/core:stage-build
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM jumpserver/core-base:20240808_054051 AS stage-build
|
||||
FROM jumpserver/core-base:20240924_031841 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
|
||||
@@ -43,12 +43,18 @@ RUN set -ex \
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
|
||||
--mount=type=bind,source=poetry.lock,target=poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
|
||||
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install --only main
|
||||
&& poetry install --only main \
|
||||
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
|
||||
&& bash clean_site_packages.sh
|
||||
|
||||
@@ -32,6 +32,8 @@ Access JumpServer in your browser at `http://your-jumpserver-ip/`
|
||||
- Username: `admin`
|
||||
- Password: `ChangeMe`
|
||||
|
||||
[](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
|
||||
|
||||
## Screenshots
|
||||
|
||||
<table style="border-collapse: collapse; border: 1px solid black;">
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -13,9 +16,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -30,9 +33,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -47,7 +50,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -28,6 +36,10 @@
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
@@ -39,3 +51,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
|
||||
@@ -14,27 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -54,14 +42,40 @@
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -34,6 +34,12 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
AIX account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
|
||||
@@ -60,6 +66,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
@@ -75,3 +86,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -14,27 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -54,14 +42,40 @@
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -36,6 +36,12 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
|
||||
@@ -62,6 +68,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
@@ -77,3 +88,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_host: "{{ jms_asset.origin_address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_gateway | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -50,9 +50,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||
username = account.username
|
||||
path = f'/{username}' if username == "root" else f'/home/{username}'
|
||||
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
@@ -130,6 +127,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
comment=f'{account.username}@{asset.address}'
|
||||
)
|
||||
records.append(recorder)
|
||||
else:
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -12,9 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
filter: "roles"
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -95,12 +95,14 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
return None, None
|
||||
|
||||
users = User.objects.filter(id__in=recipients)
|
||||
if not users:
|
||||
if not users.exists():
|
||||
return users, None
|
||||
|
||||
asset_ids = self.asset_username_mapper.keys()
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
|
||||
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
|
||||
|
||||
asset_id_map = {str(asset.id): asset for asset in assets}
|
||||
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
||||
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
||||
@@ -109,26 +111,24 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
for asset_id, username in asset_id_username:
|
||||
system_asset_username_mapper[str(asset_id)].add(username)
|
||||
|
||||
change_info = {}
|
||||
change_info = defaultdict(dict)
|
||||
for asset_id, usernames in self.asset_username_mapper.items():
|
||||
system_usernames = system_asset_username_mapper.get(asset_id)
|
||||
|
||||
if not system_usernames:
|
||||
continue
|
||||
|
||||
add_usernames = usernames - system_usernames
|
||||
remove_usernames = system_usernames - usernames
|
||||
k = f'{asset_id_map[asset_id]}[{asset_id}]'
|
||||
|
||||
if not add_usernames and not remove_usernames:
|
||||
continue
|
||||
|
||||
change_info[k] = {
|
||||
'add_usernames': ', '.join(add_usernames),
|
||||
'remove_usernames': ', '.join(remove_usernames),
|
||||
change_info[str(asset_id_map[asset_id])] = {
|
||||
'add_usernames': add_usernames,
|
||||
'remove_usernames': remove_usernames
|
||||
}
|
||||
|
||||
return users, change_info
|
||||
return users, dict(change_info)
|
||||
|
||||
@staticmethod
|
||||
def send_email_if_need(users, change_info):
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -13,9 +16,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -30,9 +33,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@@ -47,7 +50,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -28,6 +36,10 @@
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
@@ -40,6 +52,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -14,27 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -54,14 +42,40 @@
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -34,6 +34,12 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Aix account push:
|
||||
zh: '使用 Ansible 模块 user 执行 Aix 账号推送 (DES)'
|
||||
@@ -60,6 +66,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
@@ -75,3 +86,7 @@ i18n:
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -14,27 +14,15 @@
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: yes
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
when: user_info.failed
|
||||
|
||||
- name: "Add {{ account.username }} user to group"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when:
|
||||
- user_info.failed
|
||||
- params.groups
|
||||
|
||||
- name: "Set {{ account.username }} sudo setting"
|
||||
ansible.builtin.lineinfile:
|
||||
dest: /etc/sudoers
|
||||
@@ -54,14 +42,40 @@
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if home directory exists for {{ account.username }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
register: home_dir_stat
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Ensure {{ account.username }} home directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ home_dir.stdout.strip() }}"
|
||||
state: directory
|
||||
owner: "{{ account.username }}"
|
||||
group: "{{ account.username }}"
|
||||
mode: '0750'
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- home_dir_stat.stat.exists == false
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
ansible.builtin.authorized_key:
|
||||
@@ -79,7 +93,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
@@ -95,7 +109,7 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -36,6 +36,12 @@ params:
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
- name: uid
|
||||
type: str
|
||||
label: "{{ 'Params uid label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params uid help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account push:
|
||||
zh: '使用 Ansible 模块 user 执行账号推送 (sha512)'
|
||||
@@ -62,6 +68,11 @@ i18n:
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params uid help text:
|
||||
zh: '请输入用户ID'
|
||||
ja: 'ユーザーIDを入力してください'
|
||||
en: 'Please enter the user ID'
|
||||
|
||||
Modify sudo label:
|
||||
zh: '修改 sudo 权限'
|
||||
ja: 'sudo 権限を変更'
|
||||
@@ -75,4 +86,9 @@ i18n:
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
en: 'Groups'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_host: "{{ jms_asset.origin_address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_gateway | default(None) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
@@ -12,8 +15,8 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
@@ -12,4 +16,8 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
state: absent
|
||||
|
||||
@@ -13,4 +13,3 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
@@ -12,7 +15,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
@@ -11,5 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from assets.automations.ping_gateway.manager import PingGatewayManager
|
||||
from common.utils import get_logger
|
||||
@@ -13,7 +15,7 @@ class VerifyGatewayAccountManager(PingGatewayManager):
|
||||
|
||||
@staticmethod
|
||||
def before_runner_start():
|
||||
logger.info(">>> 开始执行测试网关账号可连接性任务")
|
||||
logger.info(_(">>> Start executing the task to test gateway account connectivity"))
|
||||
|
||||
def get_accounts(self, gateway):
|
||||
account_ids = self.execution.snapshot['accounts']
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.1.13 on 2024-08-26 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0005_myasset'),
|
||||
('accounts', '0003_automation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='changesecretrecord',
|
||||
name='account',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.account'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='changesecretrecord',
|
||||
name='asset',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='changesecretrecord',
|
||||
name='execution',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.automationexecution'),
|
||||
),
|
||||
]
|
||||
@@ -53,7 +53,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
)
|
||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'],
|
||||
verbose_name=_("historical Account"))
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
|
||||
|
||||
@@ -33,16 +33,15 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
|
||||
|
||||
class ChangeSecretRecord(JMSBaseModel):
|
||||
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.CASCADE)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
|
||||
execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.SET_NULL, null=True)
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.SET_NULL, null=True)
|
||||
account = models.ForeignKey('accounts.Account', on_delete=models.SET_NULL, null=True)
|
||||
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
||||
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
||||
status = models.CharField(
|
||||
max_length=16, verbose_name=_('Status'),
|
||||
default=ChangeSecretRecordStatusChoice.pending.value
|
||||
max_length=16, verbose_name=_('Status'), default=ChangeSecretRecordStatusChoice.pending.value
|
||||
)
|
||||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||
|
||||
@@ -51,4 +50,4 @@ class ChangeSecretRecord(JMSBaseModel):
|
||||
verbose_name = _("Change secret record")
|
||||
|
||||
def __str__(self):
|
||||
return self.account.__str__()
|
||||
return f'{self.account.username}@{self.asset}'
|
||||
|
||||
@@ -178,7 +178,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
instance.save()
|
||||
return instance, 'updated'
|
||||
else:
|
||||
raise serializers.ValidationError('Account already exists')
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
@@ -247,6 +247,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
'name': {'required': False},
|
||||
'source_id': {'required': False, 'allow_null': True},
|
||||
}
|
||||
fields_unimport_template = ['params']
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
|
||||
@@ -10,7 +10,7 @@ from .base import BaseAccountSerializer
|
||||
|
||||
|
||||
class PasswordRulesSerializer(serializers.Serializer):
|
||||
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
|
||||
length = serializers.IntegerField(min_value=8, max_value=36, default=16, label=_('Password length'))
|
||||
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
|
||||
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
|
||||
digit = serializers.BooleanField(default=True, label=_('Digit'))
|
||||
@@ -19,6 +19,16 @@ class PasswordRulesSerializer(serializers.Serializer):
|
||||
default='', allow_blank=True, max_length=16, label=_('Exclude symbol')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_render_help_text():
|
||||
return _("""length is the length of the password, and the range is 8 to 30.
|
||||
lowercase indicates whether the password contains lowercase letters,
|
||||
uppercase indicates whether it contains uppercase letters,
|
||||
digit indicates whether it contains numbers, and symbol indicates whether it contains special symbols.
|
||||
exclude_symbols is used to exclude specific symbols. You can fill in the symbol characters to be excluded (up to 16).
|
||||
If you do not need to exclude symbols, you can leave it blank.
|
||||
default: {"length": 16, "lowercase": true, "uppercase": true, "digit": true, "symbol": true, "exclude_symbols": ""}""")
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
|
||||
@@ -46,6 +56,7 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
'required': False
|
||||
},
|
||||
}
|
||||
fields_unimport_template = ['push_params']
|
||||
|
||||
@staticmethod
|
||||
def generate_secret(attrs):
|
||||
|
||||
@@ -75,19 +75,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
if self.initial_data.get('secret_strategy') == SecretStrategy.custom:
|
||||
return password_rules
|
||||
|
||||
length = password_rules.get('length')
|
||||
|
||||
try:
|
||||
length = int(length)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
msg = _("* Please enter the correct password length")
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
if length < 6 or length > 30:
|
||||
msg = _('* Password length range 6-30 bits')
|
||||
raise serializers.ValidationError(msg)
|
||||
|
||||
return password_rules
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import datetime
|
||||
|
||||
from celery import shared_task
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.const.crontab import CRONTAB_AT_AM_THREE
|
||||
from common.utils import get_logger, get_object_or_none, get_log_keep_day
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -22,8 +28,14 @@ def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Account execute automation'),
|
||||
activity_callback=task_activity_callback
|
||||
queue='ansible',
|
||||
verbose_name=_('Account execute automation'),
|
||||
activity_callback=task_activity_callback,
|
||||
description=_(
|
||||
"""Unified execution entry for account automation tasks: when the system performs tasks
|
||||
such as account push, password change, account verification, account collection,
|
||||
and gateway account verification, all tasks are executed through this unified entry"""
|
||||
)
|
||||
)
|
||||
def execute_account_automation_task(pid, trigger, tp):
|
||||
model = AutomationTypes.get_type_model(tp)
|
||||
@@ -48,8 +60,12 @@ def record_task_activity_callback(self, record_ids, *args, **kwargs):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Execute automation record'),
|
||||
activity_callback=record_task_activity_callback
|
||||
queue='ansible',
|
||||
verbose_name=_('Execute automation record'),
|
||||
activity_callback=record_task_activity_callback,
|
||||
description=_(
|
||||
"""When manually executing password change records, this task is used"""
|
||||
)
|
||||
)
|
||||
def execute_automation_record_task(record_ids, tp):
|
||||
from accounts.models import ChangeSecretRecord
|
||||
@@ -74,3 +90,33 @@ def execute_automation_record_task(record_ids, tp):
|
||||
}
|
||||
with tmp_to_org(record.execution.org_id):
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Clean change secret and push record period'),
|
||||
description=_(
|
||||
"""The system will periodically clean up unnecessary password change and push records,
|
||||
including their associated change tasks, execution logs, assets, and accounts. When any
|
||||
of these associated items are deleted, the corresponding password change and push records
|
||||
become invalid. Therefore, to maintain a clean and efficient database, the system will
|
||||
clean up expired records at 2 a.m daily, based on the interval specified by
|
||||
PERM_EXPIRED_CHECK_PERIODIC in the config.txt configuration file. This periodic cleanup
|
||||
mechanism helps free up storage space and enhances the security and overall performance
|
||||
of data management"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_THREE)
|
||||
def clean_change_secret_and_push_record_period():
|
||||
from accounts.models import ChangeSecretRecord
|
||||
print('Start clean change secret and push record period')
|
||||
with tmp_to_root_org():
|
||||
now = timezone.now()
|
||||
days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS')
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
records = ChangeSecretRecord.objects.filter(
|
||||
date_updated__lt=expired_day
|
||||
).filter(
|
||||
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
|
||||
)
|
||||
|
||||
records.delete()
|
||||
|
||||
@@ -22,7 +22,13 @@ def task_activity_callback(self, pid, trigger, *args, **kwargs):
|
||||
return resource_ids, org_id
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
|
||||
@shared_task(
|
||||
verbose_name=_('Execute account backup plan'),
|
||||
activity_callback=task_activity_callback,
|
||||
description=_(
|
||||
"When performing scheduled or manual account backups, this task is used"
|
||||
)
|
||||
)
|
||||
def execute_account_backup_task(pid, trigger, **kwargs):
|
||||
from accounts.models import AccountBackupAutomation
|
||||
with tmp_to_root_org():
|
||||
|
||||
@@ -26,8 +26,10 @@ def gather_asset_accounts_util(nodes, task_name):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Gather asset accounts'),
|
||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Gather asset accounts'),
|
||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
||||
description=_("Unused")
|
||||
)
|
||||
def gather_asset_accounts_task(node_ids, task_name=None):
|
||||
if task_name is None:
|
||||
|
||||
@@ -12,8 +12,12 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Push accounts to assets'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Push accounts to assets'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
|
||||
description=_(
|
||||
"When creating or modifying an account requires account push, this task is executed"
|
||||
)
|
||||
)
|
||||
def push_accounts_to_assets_task(account_ids, params=None):
|
||||
from accounts.models import PushAccountAutomation
|
||||
|
||||
@@ -21,8 +21,13 @@ __all__ = ['remove_accounts_task']
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Remove account'),
|
||||
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Remove account'),
|
||||
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None),
|
||||
description=_(
|
||||
"""When clicking "Sync deletion" in 'Console - Gather Account - Gathered accounts' this
|
||||
task will be executed"""
|
||||
)
|
||||
)
|
||||
def remove_accounts_task(gather_account_ids):
|
||||
from accounts.models import GatheredAccount
|
||||
@@ -41,7 +46,15 @@ def remove_accounts_task(gather_account_ids):
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Clean historical accounts'))
|
||||
@shared_task(
|
||||
verbose_name=_('Clean historical accounts'),
|
||||
description=_(
|
||||
"""Each time an asset account is updated, a historical account is generated, so it is
|
||||
necessary to clean up the asset account history. The system will clean up excess account
|
||||
records at 2 a.m. daily based on the configuration in the "System settings - Features -
|
||||
Account storage - Record limit"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||
@tmp_to_root_org()
|
||||
def clean_historical_accounts():
|
||||
|
||||
@@ -9,7 +9,11 @@ from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Template sync info to related accounts'),
|
||||
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None)
|
||||
activity_callback=lambda self, template_id, *args, **kwargs: (template_id, None),
|
||||
description=_(
|
||||
"""When clicking 'Sync new secret to accounts' in 'Console - Account - Templates -
|
||||
Accounts' this task will be executed"""
|
||||
)
|
||||
)
|
||||
def template_sync_related_accounts(template_id, user_id=None):
|
||||
from accounts.models import Account, AccountTemplate
|
||||
|
||||
@@ -28,7 +28,12 @@ def sync_instance(instance):
|
||||
return "succeeded", msg
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Sync secret to vault'))
|
||||
@shared_task(
|
||||
verbose_name=_('Sync secret to vault'),
|
||||
description=_(
|
||||
"When clicking 'Sync' in 'System Settings - Features - Account Storage' this task will be executed"
|
||||
)
|
||||
)
|
||||
def sync_secret_to_vault():
|
||||
if not vault_client.enabled:
|
||||
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from assets.const import GATEWAY_NAME
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
|
||||
@@ -32,13 +31,13 @@ def verify_accounts_connectivity_util(accounts, task_name):
|
||||
asset_ids = [a.asset_id for a in accounts]
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
gateways = assets.filter(platform__name=GATEWAY_NAME)
|
||||
gateways = assets.gateways()
|
||||
verify_connectivity_util(
|
||||
gateways, AutomationTypes.verify_gateway_account,
|
||||
accounts, task_name
|
||||
)
|
||||
|
||||
common_assets = assets.exclude(platform__name=GATEWAY_NAME)
|
||||
common_assets = assets.gateways(0)
|
||||
verify_connectivity_util(
|
||||
common_assets, AutomationTypes.verify_account,
|
||||
accounts, task_name
|
||||
@@ -46,8 +45,12 @@ def verify_accounts_connectivity_util(accounts, task_name):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Verify asset account availability'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
|
||||
queue="ansible",
|
||||
verbose_name=_('Verify asset account availability'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
|
||||
description=_(
|
||||
"When clicking 'Test' in 'Console - Asset details - Accounts' this task will be executed"
|
||||
)
|
||||
)
|
||||
def verify_accounts_connectivity_task(account_ids):
|
||||
from accounts.models import Account, VerifyAccountAutomation
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans 'Gather account change information' %}</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
||||
<h3></h3>
|
||||
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; text-align: left; margin-top: 20px;">
|
||||
<caption></caption>
|
||||
<tr style="background-color: #f2f2f2;">
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Added account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
|
||||
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
|
||||
{% trans 'Asset' %}
|
||||
</th>
|
||||
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
|
||||
{% trans 'Added account' %}
|
||||
</th>
|
||||
<th style="border: 1px solid #ddd; padding: 15px; text-align: left; vertical-align: top; line-height: 1.5;">
|
||||
{% trans 'Deleted account' %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for name, change in change_info.items %}
|
||||
<tr style="{% cycle 'background-color: #ebf5ff;' 'background-color: #fff;' %}">
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ name }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.add_usernames }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.remove_usernames }}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
|
||||
{{ name | safe }}
|
||||
</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
|
||||
{{ change.add_usernames | join:" " | safe }}
|
||||
</td>
|
||||
<td style="border: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top;">
|
||||
{{ change.remove_usernames | join:" " | safe }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
@@ -8,3 +8,4 @@ class ActionChoices(models.TextChoices):
|
||||
review = 'review', _('Review')
|
||||
warning = 'warning', _('Warn')
|
||||
notice = 'notice', _('Notify')
|
||||
notify_and_warn = 'notify_and_warn', _('Notify and warn')
|
||||
|
||||
@@ -62,7 +62,7 @@ class ActionAclSerializer(serializers.Serializer):
|
||||
self.set_action_choices()
|
||||
|
||||
class Meta:
|
||||
action_choices_exclude = [ActionChoices.warning]
|
||||
action_choices_exclude = [ActionChoices.warning, ActionChoices.notify_and_warn]
|
||||
|
||||
def set_action_choices(self):
|
||||
field_action = self.fields.get("action")
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@@ -22,6 +22,7 @@ from common.drf.filters import BaseFilterSet, AttrRulesFilterBackend
|
||||
from common.utils import get_logger, is_uuid
|
||||
from orgs.mixins import generics
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ...const import GATEWAY_NAME
|
||||
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -32,31 +33,32 @@ __all__ = [
|
||||
|
||||
|
||||
class AssetFilterSet(BaseFilterSet):
|
||||
platform = django_filters.CharFilter(method='filter_platform')
|
||||
exclude_platform = django_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
|
||||
domain = django_filters.CharFilter(method='filter_domain')
|
||||
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
|
||||
protocols = django_filters.CharFilter(method='filter_protocols')
|
||||
domain_enabled = django_filters.BooleanFilter(
|
||||
platform = drf_filters.CharFilter(method='filter_platform')
|
||||
is_gateway = drf_filters.BooleanFilter(method='filter_is_gateway')
|
||||
exclude_platform = drf_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
|
||||
domain = drf_filters.CharFilter(method='filter_domain')
|
||||
type = drf_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
category = drf_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
|
||||
protocols = drf_filters.CharFilter(method='filter_protocols')
|
||||
domain_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__domain_enabled", lookup_expr="exact"
|
||||
)
|
||||
ping_enabled = django_filters.BooleanFilter(
|
||||
ping_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__ping_enabled", lookup_expr="exact"
|
||||
)
|
||||
gather_facts_enabled = django_filters.BooleanFilter(
|
||||
gather_facts_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__gather_facts_enabled", lookup_expr="exact"
|
||||
)
|
||||
change_secret_enabled = django_filters.BooleanFilter(
|
||||
change_secret_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__change_secret_enabled", lookup_expr="exact"
|
||||
)
|
||||
push_account_enabled = django_filters.BooleanFilter(
|
||||
push_account_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__push_account_enabled", lookup_expr="exact"
|
||||
)
|
||||
verify_account_enabled = django_filters.BooleanFilter(
|
||||
verify_account_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__verify_account_enabled", lookup_expr="exact"
|
||||
)
|
||||
gather_accounts_enabled = django_filters.BooleanFilter(
|
||||
gather_accounts_enabled = drf_filters.BooleanFilter(
|
||||
field_name="platform__automation__gather_accounts_enabled", lookup_expr="exact"
|
||||
)
|
||||
|
||||
@@ -71,9 +73,16 @@ class AssetFilterSet(BaseFilterSet):
|
||||
def filter_platform(queryset, name, value):
|
||||
if value.isdigit():
|
||||
return queryset.filter(platform_id=value)
|
||||
elif value == GATEWAY_NAME:
|
||||
return queryset.filter(platform__name__istartswith=GATEWAY_NAME)
|
||||
else:
|
||||
return queryset.filter(platform__name=value)
|
||||
|
||||
@staticmethod
|
||||
def filter_is_gateway(queryset, name, value):
|
||||
queryset = queryset.gateways(value)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def filter_domain(queryset, name, value):
|
||||
if is_uuid(value):
|
||||
@@ -298,6 +307,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
def check_permissions(self, request):
|
||||
action_perm_require = {
|
||||
"refresh": "assets.refresh_assethardwareinfo",
|
||||
"test": "assets.test_assetconnectivity",
|
||||
}
|
||||
_action = request.data.get("action")
|
||||
perm_required = action_perm_require.get(_action)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.db.models import Count
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import generics
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
@@ -14,6 +15,14 @@ from common.serializers import GroupedChoiceSerializer
|
||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet']
|
||||
|
||||
|
||||
class PlatformFilter(filters.FilterSet):
|
||||
name__startswith = filters.CharFilter(field_name='name', lookup_expr='istartswith')
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['name', 'category', 'type']
|
||||
|
||||
|
||||
class AssetPlatformViewSet(JMSModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_classes = {
|
||||
@@ -21,7 +30,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||
'list': PlatformListSerializer,
|
||||
'categories': GroupedChoiceSerializer,
|
||||
}
|
||||
filterset_fields = ['name', 'category', 'type']
|
||||
filterset_class = PlatformFilter
|
||||
search_fields = ['name']
|
||||
ordering = ['-internal', 'name']
|
||||
rbac_perms = {
|
||||
|
||||
@@ -170,6 +170,7 @@ class BasePlaybookManager:
|
||||
result = self.write_cert_to_file(
|
||||
os.path.join(cert_dir, f), specific.get(f)
|
||||
)
|
||||
os.chmod(result, 0o600)
|
||||
host['jms_asset']['secret_info'][f] = result
|
||||
return host
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -12,9 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@@ -11,6 +15,10 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
|
||||
@@ -13,4 +13,3 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@@ -12,7 +15,7 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
|
||||
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -11,5 +15,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -115,7 +115,7 @@ class PingGatewayManager:
|
||||
|
||||
@staticmethod
|
||||
def before_runner_start():
|
||||
print(">>> 开始执行测试网关可连接性任务")
|
||||
print(_(">>> Start executing the task to test gateway connectivity"))
|
||||
|
||||
def get_accounts(self, gateway):
|
||||
account = gateway.select_account
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .automation import *
|
||||
from .base import *
|
||||
from .category import *
|
||||
from .database import *
|
||||
from .host import *
|
||||
from .platform import *
|
||||
from .protocol import *
|
||||
|
||||
@@ -112,7 +112,7 @@ class BaseType(TextChoices):
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
if not settings.XPACK_ENABLED:
|
||||
if not settings.XPACK_LICENSE_IS_VALID:
|
||||
choices = [(tp.value, tp.label) for tp in cls.get_community_types()]
|
||||
else:
|
||||
choices = cls.choices
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from django.db.models import TextChoices
|
||||
|
||||
from .base import BaseType
|
||||
|
||||
|
||||
@@ -120,3 +122,10 @@ class DatabaseTypes(BaseType):
|
||||
cls.MYSQL, cls.MARIADB, cls.POSTGRESQL,
|
||||
cls.MONGODB, cls.REDIS,
|
||||
]
|
||||
|
||||
|
||||
class PostgresqlSSLMode(TextChoices):
|
||||
PREFER = 'prefer', 'Prefer'
|
||||
REQUIRE = 'require', 'Require'
|
||||
VERIFY_CA = 'verify-ca', 'Verify CA'
|
||||
VERIFY_FULL = 'verify-full', 'Verify Full'
|
||||
|
||||
@@ -45,6 +45,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||
'default': False,
|
||||
'label': _('Old SSH version'),
|
||||
'help_text': _('Old SSH version like openssh 5.x or 6.x')
|
||||
},
|
||||
'nc': {
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'label': 'Netcat (nc)',
|
||||
'help_text': _('Netcat help text')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
23
apps/assets/migrations/0006_database_pg_ssl_mode.py
Normal file
23
apps/assets/migrations/0006_database_pg_ssl_mode.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.13 on 2024-09-13 08:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0005_myasset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='database',
|
||||
name='pg_ssl_mode',
|
||||
field=models.CharField(choices=[
|
||||
('prefer', 'Prefer'),
|
||||
('require', 'Require'),
|
||||
('verify-ca', 'Verify CA'),
|
||||
('verify-full', 'Verify Full')
|
||||
], default='prefer',
|
||||
max_length=16, verbose_name='Postgresql SSL mode'),
|
||||
),
|
||||
]
|
||||
@@ -38,6 +38,13 @@ class AssetQuerySet(models.QuerySet):
|
||||
def valid(self):
|
||||
return self.active()
|
||||
|
||||
def gateways(self, is_gateway=1):
|
||||
kwargs = {'platform__name__startswith': 'Gateway'}
|
||||
if is_gateway:
|
||||
return self.filter(**kwargs)
|
||||
else:
|
||||
return self.exclude(**kwargs)
|
||||
|
||||
def has_protocol(self, name):
|
||||
return self.filter(protocols__contains=name)
|
||||
|
||||
@@ -158,10 +165,16 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
address = models.CharField(max_length=767, verbose_name=_('Address'), db_index=True)
|
||||
platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets',
|
||||
verbose_name=_("Zone"), on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
|
||||
platform = models.ForeignKey(
|
||||
Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets'
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
"assets.Domain", null=True, blank=True, related_name='assets',
|
||||
verbose_name=_("Zone"), on_delete=models.SET_NULL
|
||||
)
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")
|
||||
)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
||||
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.const import PostgresqlSSLMode
|
||||
from common.db.fields import EncryptTextField
|
||||
from .common import Asset
|
||||
|
||||
@@ -12,6 +13,10 @@ class Database(Asset):
|
||||
client_cert = EncryptTextField(verbose_name=_("Client cert"), blank=True)
|
||||
client_key = EncryptTextField(verbose_name=_("Client key"), blank=True)
|
||||
allow_invalid_cert = models.BooleanField(default=False, verbose_name=_('Allow invalid cert'))
|
||||
pg_ssl_mode = models.CharField(
|
||||
max_length=16, choices=PostgresqlSSLMode.choices,
|
||||
default=PostgresqlSSLMode.PREFER, verbose_name=_('Postgresql SSL mode')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name)
|
||||
|
||||
@@ -31,7 +31,7 @@ class Domain(LabeledMixin, JMSOrgBaseModel):
|
||||
|
||||
@lazyproperty
|
||||
def assets_amount(self):
|
||||
return self.assets.exclude(platform__name='Gateway').count()
|
||||
return self.assets.gateways(0).count()
|
||||
|
||||
def random_gateway(self):
|
||||
gateways = [gw for gw in self.active_gateways if gw.is_connective]
|
||||
|
||||
@@ -16,7 +16,7 @@ __all__ = ['Gateway']
|
||||
class GatewayManager(OrgManager):
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(platform__name=GATEWAY_NAME)
|
||||
queryset = queryset.filter(platform__name__startswith=GATEWAY_NAME)
|
||||
return queryset
|
||||
|
||||
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||
@@ -33,10 +33,6 @@ class Gateway(Host):
|
||||
proxy = True
|
||||
verbose_name = _("Gateway")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.platform = self.default_platform()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def default_platform(cls):
|
||||
return Platform.objects.get(name=GATEWAY_NAME, internal=True)
|
||||
|
||||
@@ -18,7 +18,7 @@ from common.serializers.fields import LabeledChoiceField
|
||||
from labels.models import Label
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ...const import Category, AllTypes
|
||||
from ...models import Asset, Node, Platform, Protocol
|
||||
from ...models import Asset, Node, Platform, Protocol, Host, Device, Database, Cloud, Web, Custom
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
@@ -31,6 +31,12 @@ __all__ = [
|
||||
class AssetProtocolsSerializer(serializers.ModelSerializer):
|
||||
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=0)
|
||||
|
||||
def get_render_help_text(self):
|
||||
if self.parent and self.parent.many:
|
||||
return _('Protocols, format is ["protocol/port"]')
|
||||
else:
|
||||
return _('Protocol, format is name/port')
|
||||
|
||||
def to_file_representation(self, data):
|
||||
return '{name}/{port}'.format(**data)
|
||||
|
||||
@@ -97,6 +103,9 @@ class AssetAccountSerializer(AccountSerializer):
|
||||
attrs = super().validate(attrs)
|
||||
return self.set_secret(attrs)
|
||||
|
||||
def get_render_help_text(self):
|
||||
return _('Accounts, format [{"name": "x", "username": "x", "secret": "x", "secret_type": "password"}]')
|
||||
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields = [
|
||||
f for f in AccountSerializer.Meta.fields
|
||||
@@ -121,19 +130,30 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class NodeDisplaySerializer(serializers.ListField):
|
||||
def get_render_help_text(self):
|
||||
return _('Node path, format ["/org_name/node_name"], if node not exist, will create it')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data
|
||||
|
||||
def to_representation(self, data):
|
||||
return data
|
||||
|
||||
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
|
||||
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
|
||||
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
|
||||
_accounts = None
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields_mini = ['id', 'name', 'address']
|
||||
fields_small = fields_mini + ['is_active', 'comment']
|
||||
fields_fk = ['domain', 'platform']
|
||||
fields_mini = ['id', 'name', 'address'] + fields_fk
|
||||
fields_small = fields_mini + ['is_active', 'comment']
|
||||
fields_m2m = [
|
||||
'nodes', 'labels', 'protocols',
|
||||
'nodes_display', 'accounts',
|
||||
@@ -289,6 +309,17 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
})
|
||||
return protocols_data_map.values()
|
||||
|
||||
def validate_platform(self, platform_data):
|
||||
check_models = {Host, Device, Database, Cloud, Web, Custom}
|
||||
if self.Meta.model not in check_models:
|
||||
return platform_data
|
||||
model_name = self.Meta.model.__name__.lower()
|
||||
if model_name != platform_data.category:
|
||||
raise serializers.ValidationError({
|
||||
'platform': f"Platform does not match: {platform_data.name}"
|
||||
})
|
||||
return platform_data
|
||||
|
||||
@staticmethod
|
||||
def update_account_su_from(accounts, include_su_from_accounts):
|
||||
if not include_su_from_accounts:
|
||||
|
||||
@@ -16,6 +16,7 @@ class CustomSerializer(AssetSerializer):
|
||||
class Meta(AssetSerializer.Meta):
|
||||
model = Custom
|
||||
fields = AssetSerializer.Meta.fields + ['custom_info']
|
||||
fields_unimport_template = ['custom_info']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -16,9 +16,14 @@ class DatabaseSerializer(AssetSerializer):
|
||||
model = Database
|
||||
extra_fields = [
|
||||
'db_name', 'use_ssl', 'ca_cert', 'client_cert',
|
||||
'client_key', 'allow_invalid_cert'
|
||||
'client_key', 'allow_invalid_cert', 'pg_ssl_mode'
|
||||
]
|
||||
fields = AssetSerializer.Meta.fields + extra_fields
|
||||
extra_kwargs = {
|
||||
'ca_cert': {'help_text': _('CA cert help text')},
|
||||
'pg_ssl_mode': {'help_text': _('Postgresql ssl model help text')},
|
||||
}
|
||||
extra_kwargs.update(AssetSerializer.Meta.extra_kwargs)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -68,7 +68,7 @@ class DomainListSerializer(DomainSerializer):
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.annotate(
|
||||
assets_amount=Count('assets', filter=~Q(assets__platform__name='Gateway'), distinct=True),
|
||||
assets_amount=Count('assets', filter=~Q(assets__platform__name__startswith='Gateway'), distinct=True),
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ class GatewaySerializer(HostSerializer):
|
||||
class Meta(HostSerializer.Meta):
|
||||
model = Gateway
|
||||
|
||||
def validate_platform(self, p):
|
||||
if not p.name.startswith('Gateway'):
|
||||
raise serializers.ValidationError(_('The platform must start with Gateway'))
|
||||
return p
|
||||
|
||||
def validate_name(self, value):
|
||||
queryset = Asset.objects.filter(name=value)
|
||||
if self.instance:
|
||||
|
||||
@@ -147,6 +147,10 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
|
||||
name, port = data.split('/')
|
||||
return {'name': name, 'port': port}
|
||||
|
||||
@staticmethod
|
||||
def get_render_help_text():
|
||||
return _('Protocols, format is ["protocol/port"]')
|
||||
|
||||
|
||||
class PlatformCustomField(serializers.Serializer):
|
||||
TYPE_CHOICES = [(t, t) for t, c in type_field_map.items()]
|
||||
|
||||
@@ -21,8 +21,10 @@ def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue='ansible', verbose_name=_('Asset execute automation'),
|
||||
activity_callback=task_activity_callback
|
||||
queue='ansible',
|
||||
verbose_name=_('Asset execute automation'),
|
||||
activity_callback=task_activity_callback,
|
||||
description=_("Unused")
|
||||
)
|
||||
def execute_asset_automation_task(pid, trigger, tp):
|
||||
model = AutomationTypes.get_type_model(tp)
|
||||
|
||||
@@ -18,8 +18,13 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Gather assets facts'),
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
|
||||
queue="ansible",
|
||||
verbose_name=_('Gather assets facts'),
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
|
||||
description=_(
|
||||
"""When clicking 'Refresh hardware info' in 'Console - Asset Details - Basic' this task
|
||||
will be executed"""
|
||||
)
|
||||
)
|
||||
def gather_assets_facts_task(asset_ids, org_id, task_name=None):
|
||||
from assets.models import GatherFactsAutomation
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from assets.utils import check_node_assets_amount
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
from common.utils import get_logger
|
||||
from common.utils.lock import AcquireFailed
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import tmp_to_org
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from assets.utils import check_node_assets_amount
|
||||
|
||||
from common.utils.lock import AcquireFailed
|
||||
from common.utils import get_logger
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Check the amount of assets under the node'))
|
||||
@shared_task(
|
||||
verbose_name=_('Check the amount of assets under the node'),
|
||||
description=_(
|
||||
"""Manually verifying asset quantities updates the asset count for nodes under the
|
||||
current organization. This task will be called in the following two cases: when updating
|
||||
nodes and when the number of nodes exceeds 100"""
|
||||
)
|
||||
)
|
||||
def check_node_assets_amount_task(org_id=None):
|
||||
if org_id is None:
|
||||
orgs = Organization.objects.all()
|
||||
@@ -30,7 +36,13 @@ def check_node_assets_amount_task(org_id=None):
|
||||
logger.error(error)
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Periodic check the amount of assets under the node'))
|
||||
@shared_task(
|
||||
verbose_name=_('Periodic check the amount of assets under the node'),
|
||||
description=_(
|
||||
"""Schedule the check_node_assets_amount_task to periodically update the asset count of
|
||||
all nodes under all organizations"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||
def check_node_assets_amount_period_task():
|
||||
check_node_assets_amount_task()
|
||||
|
||||
@@ -17,8 +17,12 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Test assets connectivity'), queue='ansible',
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
|
||||
verbose_name=_('Test assets connectivity'),
|
||||
queue='ansible',
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
|
||||
description=_(
|
||||
"When clicking 'Test Asset Connectivity' in 'Asset Details - Basic Settings' this task will be executed"
|
||||
)
|
||||
)
|
||||
def test_assets_connectivity_task(asset_ids, org_id, task_name=None):
|
||||
from assets.models import PingAutomation
|
||||
|
||||
@@ -16,8 +16,12 @@ __all__ = [
|
||||
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Test gateways connectivity'), queue='ansible',
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id)
|
||||
verbose_name=_('Test gateways connectivity'),
|
||||
queue='ansible',
|
||||
activity_callback=lambda self, asset_ids, org_id, *args, **kwargs: (asset_ids, org_id),
|
||||
description=_(
|
||||
"When clicking 'Test Connection' in 'Domain Details - Gateway' this task will be executed"
|
||||
)
|
||||
)
|
||||
def test_gateways_connectivity_task(asset_ids, org_id, local_port, task_name=None):
|
||||
from assets.models import PingAutomation
|
||||
@@ -33,4 +37,5 @@ def test_gateways_connectivity_task(asset_ids, org_id, local_port, task_name=Non
|
||||
def test_gateways_connectivity_manual(gateway_ids, local_port):
|
||||
task_name = gettext_noop("Test gateways connectivity")
|
||||
gateway_ids = [str(i) for i in gateway_ids]
|
||||
return test_gateways_connectivity_task.delay(gateway_ids, str(current_org.id), local_port, task_name)
|
||||
return test_gateways_connectivity_task.delay(gateway_ids, str(current_org.id), local_port,
|
||||
task_name)
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
from .k8s import *
|
||||
from .node import *
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
from kubernetes import client
|
||||
from kubernetes.client import api_client
|
||||
from kubernetes.client.api import core_v1_api
|
||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..const import CloudTypes, Category
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class KubernetesClient:
|
||||
def __init__(self, asset, token):
|
||||
self.url = asset.address
|
||||
self.token = token or ''
|
||||
self.server = self.get_gateway_server(asset)
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
configuration = client.Configuration()
|
||||
scheme = urlparse(self.url).scheme
|
||||
if not self.server:
|
||||
host = self.url
|
||||
else:
|
||||
host = f'{scheme}://127.0.0.1:{self.server.local_bind_port}'
|
||||
configuration.host = host
|
||||
configuration.verify_ssl = False
|
||||
configuration.api_key = {"authorization": "Bearer " + self.token}
|
||||
c = api_client.ApiClient(configuration=configuration)
|
||||
api = core_v1_api.CoreV1Api(c)
|
||||
return api
|
||||
|
||||
def get_namespaces(self):
|
||||
namespaces = []
|
||||
resp = self.api.list_namespace()
|
||||
for ns in resp.items:
|
||||
namespaces.append(ns.metadata.name)
|
||||
return namespaces
|
||||
|
||||
def get_pods(self, namespace):
|
||||
pods = []
|
||||
resp = self.api.list_namespaced_pod(namespace)
|
||||
for pd in resp.items:
|
||||
pods.append(pd.metadata.name)
|
||||
return pods
|
||||
|
||||
def get_containers(self, namespace, pod_name):
|
||||
containers = []
|
||||
resp = self.api.read_namespaced_pod(pod_name, namespace)
|
||||
for container in resp.spec.containers:
|
||||
containers.append(container.name)
|
||||
return containers
|
||||
|
||||
@staticmethod
|
||||
def get_gateway_server(asset):
|
||||
gateway = None
|
||||
if not asset.is_gateway and asset.domain:
|
||||
gateway = asset.domain.select_gateway()
|
||||
|
||||
if not gateway:
|
||||
return
|
||||
|
||||
remote_bind_address = (
|
||||
urlparse(asset.address).hostname,
|
||||
urlparse(asset.address).port or 443
|
||||
)
|
||||
server = SSHTunnelForwarder(
|
||||
(gateway.address, gateway.port),
|
||||
ssh_username=gateway.username,
|
||||
ssh_password=gateway.password,
|
||||
ssh_pkey=gateway.private_key_path,
|
||||
remote_bind_address=remote_bind_address
|
||||
)
|
||||
try:
|
||||
server.start()
|
||||
except BaseSSHTunnelForwarderError:
|
||||
err_msg = 'Gateway is not active: %s' % asset.get('name', '')
|
||||
print('\033[31m %s \033[0m\n' % err_msg)
|
||||
return server
|
||||
|
||||
def run(self, tp, *args):
|
||||
func_name = f'get_{tp}s'
|
||||
data = []
|
||||
if hasattr(self, func_name):
|
||||
try:
|
||||
data = getattr(self, func_name)(*args)
|
||||
except Exception as e:
|
||||
logger.error(f'K8S tree get {tp} error: {e}')
|
||||
|
||||
if self.server:
|
||||
self.server.stop()
|
||||
return data
|
||||
|
||||
|
||||
class KubernetesTree:
|
||||
def __init__(self, asset, secret):
|
||||
self.asset = asset
|
||||
self.secret = secret
|
||||
|
||||
def as_asset_tree_node(self):
|
||||
i = str(self.asset.id)
|
||||
name = str(self.asset)
|
||||
node = self.create_tree_node(
|
||||
i, i, name, 'asset', icon='k8s', is_open=True,
|
||||
)
|
||||
return node
|
||||
|
||||
def as_namespace_node(self, name, tp):
|
||||
i = urlencode({'namespace': name})
|
||||
pid = str(self.asset.id)
|
||||
node = self.create_tree_node(i, pid, name, tp, icon='cloud')
|
||||
return node
|
||||
|
||||
def as_pod_tree_node(self, namespace, name, tp):
|
||||
pid = urlencode({'namespace': namespace})
|
||||
i = urlencode({'namespace': namespace, 'pod': name})
|
||||
node = self.create_tree_node(i, pid, name, tp, icon='cloud')
|
||||
return node
|
||||
|
||||
def as_container_tree_node(self, namespace, pod, name, tp):
|
||||
pid = urlencode({'namespace': namespace, 'pod': pod})
|
||||
i = urlencode({'namespace': namespace, 'pod': pod, 'container': name})
|
||||
node = self.create_tree_node(
|
||||
i, pid, name, tp, icon='cloud', is_container=True
|
||||
)
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def create_tree_node(id_, pid, name, identity, icon='', is_container=False, is_open=False):
|
||||
node = {
|
||||
'id': id_,
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': pid,
|
||||
'isParent': not is_container,
|
||||
'open': is_open,
|
||||
'iconSkin': icon,
|
||||
'meta': {
|
||||
'type': 'k8s',
|
||||
'data': {
|
||||
'category': Category.CLOUD,
|
||||
'type': CloudTypes.K8S,
|
||||
'identity': identity
|
||||
}
|
||||
}
|
||||
}
|
||||
return node
|
||||
|
||||
def async_tree_node(self, namespace, pod):
|
||||
tree = []
|
||||
k8s_client = KubernetesClient(self.asset, self.secret)
|
||||
if pod:
|
||||
tp = 'container'
|
||||
containers = k8s_client.run(
|
||||
tp, namespace, pod
|
||||
)
|
||||
for container in containers:
|
||||
container_node = self.as_container_tree_node(
|
||||
namespace, pod, container, tp
|
||||
)
|
||||
tree.append(container_node)
|
||||
elif namespace:
|
||||
tp = 'pod'
|
||||
pods = k8s_client.run(tp, namespace)
|
||||
for pod in pods:
|
||||
pod_node = self.as_pod_tree_node(namespace, pod, tp)
|
||||
tree.append(pod_node)
|
||||
else:
|
||||
tp = 'namespace'
|
||||
namespaces = k8s_client.run(tp)
|
||||
for namespace in namespaces:
|
||||
namespace_node = self.as_namespace_node(namespace, tp)
|
||||
tree.append(namespace_node)
|
||||
return tree
|
||||
@@ -146,7 +146,9 @@ class MyLoginLogViewSet(UserLoginCommonMixin, OrgReadonlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
qs = qs.filter(username=self.request.user.username)
|
||||
username = self.request.user.username
|
||||
q = Q(username=username) | Q(username__icontains=f'({username})')
|
||||
qs = qs.filter(q)
|
||||
return qs
|
||||
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ class OperateLogStore(object):
|
||||
@classmethod
|
||||
def convert_diff_friendly(cls, op_log):
|
||||
diff_list = list()
|
||||
# 标记翻译字符串
|
||||
labels = _("labels")
|
||||
operate_log_id = _("operate_log_id")
|
||||
handler = cls._get_special_handler(op_log.resource_type)
|
||||
for k, v in op_log.diff.items():
|
||||
before_value, after_value = cls.split_value(v)
|
||||
|
||||
@@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
|
||||
'PermedAsset', 'PermedAccount', 'MenuPermission',
|
||||
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
||||
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
||||
'FavoriteAsset', 'ChangeSecretRecord'
|
||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider',
|
||||
}
|
||||
include_models = {'UserSession'}
|
||||
for i, app in enumerate(apps.get_models(), 1):
|
||||
|
||||
@@ -128,7 +128,15 @@ def clean_expired_session_period():
|
||||
logger.info("Clean session replay done")
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Clean audits session task log'))
|
||||
@shared_task(
|
||||
verbose_name=_('Clean audits session task log'),
|
||||
description=_(
|
||||
"""Since the system generates login logs, operation logs, file upload logs, activity
|
||||
logs, Celery execution logs, session recordings, command records, and password change
|
||||
logs, it will perform cleanup of records that exceed the time limit according to the
|
||||
'Tasks - Regular clean-up' in the system settings at 2 a.m daily"""
|
||||
)
|
||||
)
|
||||
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||
def clean_audits_log_period():
|
||||
print("Start clean audit session task log")
|
||||
@@ -142,7 +150,13 @@ def clean_audits_log_period():
|
||||
clean_password_change_log_period()
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Upload FTP file to external storage'))
|
||||
@shared_task(
|
||||
verbose_name=_('Upload FTP file to external storage'),
|
||||
description=_(
|
||||
"""If SERVER_REPLAY_STORAGE is configured, files uploaded through file management will be
|
||||
synchronized to external storage"""
|
||||
)
|
||||
)
|
||||
def upload_ftp_file_to_external_storage(ftp_log_id, file_name):
|
||||
logger.info(f'Start upload FTP file record to external storage: {ftp_log_id} - {file_name}')
|
||||
ftp_log = FTPLog.objects.filter(id=ftp_log_id).first()
|
||||
|
||||
@@ -472,6 +472,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
rbac_perms = {
|
||||
'create': 'authentication.add_superconnectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken',
|
||||
'check': 'authentication.view_superconnectiontoken',
|
||||
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
|
||||
'get_applet_info': 'authentication.view_superconnectiontoken',
|
||||
'release_applet_account': 'authentication.view_superconnectiontoken',
|
||||
@@ -484,6 +485,28 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
def get_user(self, serializer):
|
||||
return serializer.validated_data.get('user')
|
||||
|
||||
@action(methods=['GET'], detail=True, url_path='check')
|
||||
def check(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
data = {
|
||||
"detail": "OK",
|
||||
"code": "perm_ok",
|
||||
"expired": instance.is_expired
|
||||
}
|
||||
try:
|
||||
self._validate_perm(
|
||||
instance.user,
|
||||
instance.asset,
|
||||
instance.account,
|
||||
instance.protocol
|
||||
)
|
||||
except JMSException as e:
|
||||
data['code'] = e.detail.code
|
||||
data['detail'] = str(e.detail)
|
||||
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=['PATCH'], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
from common.utils.timezone import as_current_tz
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# coding:utf-8
|
||||
#
|
||||
|
||||
import abc
|
||||
import ldap
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
@@ -15,13 +15,16 @@ from .base import JMSBaseAuthBackend
|
||||
logger = _LDAPConfig.get_logger()
|
||||
|
||||
|
||||
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
||||
"""
|
||||
Override this class to override _LDAPUser to LDAPUser
|
||||
"""
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_LDAP
|
||||
class LDAPBaseBackend(LDAPBackend):
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_enabled(self):
|
||||
raise NotImplementedError('is_enabled')
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def is_user_login_only_in_users(self):
|
||||
raise NotImplementedError('is_authenticated')
|
||||
|
||||
def get_or_build_user(self, username, ldap_user):
|
||||
"""
|
||||
@@ -56,38 +59,6 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
||||
|
||||
return user, built
|
||||
|
||||
def pre_check(self, username, password):
|
||||
if not settings.AUTH_LDAP:
|
||||
error = 'Not enabled auth ldap'
|
||||
return False, error
|
||||
if not username:
|
||||
error = 'Username is None'
|
||||
return False, error
|
||||
if not password:
|
||||
error = 'Password is None'
|
||||
return False, error
|
||||
if settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS:
|
||||
user_model = self.get_user_model()
|
||||
exist = user_model.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
error = 'user ({}) is not in the user list'.format(username)
|
||||
return False, error
|
||||
return True, ''
|
||||
|
||||
def authenticate(self, request=None, username=None, password=None, **kwargs):
|
||||
logger.info('Authentication LDAP backend')
|
||||
if username is None or password is None:
|
||||
logger.info('No username or password')
|
||||
return None
|
||||
match, msg = self.pre_check(username, password)
|
||||
if not match:
|
||||
logger.info('Authenticate failed: {}'.format(msg))
|
||||
return None
|
||||
ldap_user = LDAPUser(self, username=username.strip(), request=request)
|
||||
user = self.authenticate_ldap_user(ldap_user, password)
|
||||
logger.info('Authenticate user: {}'.format(user))
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
def get_user(self, user_id):
|
||||
user = None
|
||||
try:
|
||||
@@ -111,6 +82,67 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
|
||||
user = ldap_user.populate_user()
|
||||
return user
|
||||
|
||||
def authenticate(self, request=None, username=None, password=None, **kwargs):
|
||||
logger.info('Authentication LDAP backend')
|
||||
if username is None or password is None:
|
||||
logger.info('No username or password')
|
||||
return None
|
||||
match, msg = self.pre_check(username, password)
|
||||
if not match:
|
||||
logger.info('Authenticate failed: {}'.format(msg))
|
||||
return None
|
||||
ldap_user = LDAPUser(self, username=username.strip(), request=request)
|
||||
user = self.authenticate_ldap_user(ldap_user, password)
|
||||
logger.info('Authenticate user: {}'.format(user))
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
def pre_check(self, username, password):
|
||||
if not self.is_enabled():
|
||||
error = 'Not enabled auth ldap'
|
||||
return False, error
|
||||
if not username:
|
||||
error = 'Username is None'
|
||||
return False, error
|
||||
if not password:
|
||||
error = 'Password is None'
|
||||
return False, error
|
||||
if self.is_user_login_only_in_users:
|
||||
user_model = self.get_user_model()
|
||||
exist = user_model.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
error = 'user ({}) is not in the user list'.format(username)
|
||||
return False, error
|
||||
return True, ''
|
||||
|
||||
|
||||
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
|
||||
"""
|
||||
Override this class to override _LDAPUser to LDAPUser
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_LDAP
|
||||
|
||||
@property
|
||||
def is_user_login_only_in_users(self):
|
||||
return settings.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
|
||||
|
||||
|
||||
class LDAPHAAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
|
||||
"""
|
||||
Override this class to override _LDAPUser to LDAPUser
|
||||
"""
|
||||
settings_prefix = "AUTH_LDAP_HA_"
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_LDAP_HA
|
||||
|
||||
@property
|
||||
def is_user_login_only_in_users(self):
|
||||
return settings.AUTH_LDAP_HA_USER_LOGIN_ONLY_IN_USERS
|
||||
|
||||
|
||||
class LDAPUser(_LDAPUser):
|
||||
|
||||
@@ -126,13 +158,18 @@ class LDAPUser(_LDAPUser):
|
||||
configuration in the settings.py file
|
||||
is configured with a `lambda` problem value
|
||||
"""
|
||||
|
||||
if isinstance(self.backend, LDAPAuthorizationBackend):
|
||||
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
|
||||
search_ou = settings.AUTH_LDAP_SEARCH_OU
|
||||
else:
|
||||
search_filter = settings.AUTH_LDAP_HA_SEARCH_FILTER
|
||||
search_ou = settings.AUTH_LDAP_HA_SEARCH_OU
|
||||
user_search_union = [
|
||||
LDAPSearch(
|
||||
USER_SEARCH, ldap.SCOPE_SUBTREE,
|
||||
settings.AUTH_LDAP_SEARCH_FILTER
|
||||
search_filter
|
||||
)
|
||||
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|")
|
||||
for USER_SEARCH in str(search_ou).split("|")
|
||||
]
|
||||
|
||||
search = LDAPSearchUnion(*user_search_union)
|
||||
@@ -169,7 +206,8 @@ class LDAPUser(_LDAPUser):
|
||||
else:
|
||||
value = is_true(value)
|
||||
except LookupError:
|
||||
logger.warning("{} does not have a value for the attribute {}".format(self.dn, attr))
|
||||
logger.warning(
|
||||
"{} does not have a value for the attribute {}".format(self.dn, attr))
|
||||
else:
|
||||
if not hasattr(self._user, field):
|
||||
continue
|
||||
|
||||
@@ -8,27 +8,26 @@
|
||||
"""
|
||||
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from rest_framework.exceptions import ParseError
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
from common.utils import get_logger
|
||||
from authentication.signals import user_auth_success, user_auth_failed
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token
|
||||
from .decorator import ssl_verification
|
||||
from .signals import (
|
||||
openid_create_or_update_user
|
||||
)
|
||||
from authentication.signals import user_auth_success, user_auth_failed
|
||||
from .utils import validate_and_return_id_token
|
||||
from ..base import JMSBaseAuthBackend
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -55,16 +54,17 @@ class UserMixin:
|
||||
logger.debug(log_prompt.format(user_attrs))
|
||||
|
||||
username = user_attrs.get('username')
|
||||
name = user_attrs.get('name')
|
||||
groups = user_attrs.pop('groups', None)
|
||||
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults=user_attrs
|
||||
)
|
||||
user_attrs['groups'] = groups
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => openid create or update user"))
|
||||
openid_create_or_update_user.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
name=name, username=username, email=email
|
||||
sender=self.__class__, request=request, user=user,
|
||||
created=created, attrs=user_attrs,
|
||||
)
|
||||
return user, created
|
||||
|
||||
@@ -269,7 +269,8 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, timeout=request_timeout)
|
||||
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload,
|
||||
timeout=request_timeout)
|
||||
try:
|
||||
token_response.raise_for_status()
|
||||
token_response_data = token_response.json()
|
||||
|
||||
@@ -17,13 +17,16 @@ import time
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import IntegrityError
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import urlencode
|
||||
from django.views.generic import View
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from authentication.views.mixins import FlashMessageMixin
|
||||
from common.utils import safe_next_url
|
||||
from .utils import get_logger
|
||||
|
||||
@@ -113,7 +116,7 @@ class OIDCAuthRequestView(View):
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OIDCAuthCallbackView(View):
|
||||
class OIDCAuthCallbackView(View, FlashMessageMixin):
|
||||
""" Allows to complete the authentication process.
|
||||
|
||||
This view acts as the main endpoint to complete the authentication process involving the OIDC
|
||||
@@ -165,7 +168,13 @@ class OIDCAuthCallbackView(View):
|
||||
next_url = request.session.get('oidc_auth_next_url', None)
|
||||
code_verifier = request.session.get('oidc_auth_code_verifier', None)
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
|
||||
try:
|
||||
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
|
||||
except IntegrityError:
|
||||
title = _("OpenID Error")
|
||||
msg = _('Please check if a user with the same username or email already exists')
|
||||
response = self.get_failed_response('/', title, msg)
|
||||
return response
|
||||
if user:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
|
||||
@@ -27,9 +27,13 @@ class SAML2Backend(JMSModelBackend):
|
||||
log_prompt = "Get or Create user [SAML2Backend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
groups = saml_user_data.pop('groups', None)
|
||||
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=saml_user_data['username'], defaults=saml_user_data
|
||||
)
|
||||
|
||||
saml_user_data['groups'] = groups
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
|
||||
logger.debug(log_prompt.format("Send signal => saml2 create or update user"))
|
||||
|
||||
@@ -3,8 +3,10 @@ from urllib import parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.db import IntegrityError
|
||||
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
@@ -14,6 +16,7 @@ from onelogin.saml2.idp_metadata_parser import (
|
||||
dict_deep_merge
|
||||
)
|
||||
|
||||
from authentication.views.mixins import FlashMessageMixin
|
||||
from common.utils import get_logger
|
||||
from .settings import JmsSaml2Settings
|
||||
|
||||
@@ -87,6 +90,7 @@ class PrepareRequestMixin:
|
||||
('name', 'name', False),
|
||||
('phone', 'phone', False),
|
||||
('comment', 'comment', False),
|
||||
('groups', 'groups', False),
|
||||
)
|
||||
attr_list = []
|
||||
for name, friend_name, is_required in need_attrs:
|
||||
@@ -185,7 +189,7 @@ class PrepareRequestMixin:
|
||||
user_attrs = {}
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
attrs = saml_instance.get_attributes()
|
||||
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
|
||||
valid_attrs = ['username', 'name', 'email', 'comment', 'phone', 'groups']
|
||||
|
||||
for attr, value in attrs.items():
|
||||
attr = attr.rsplit('/', 1)[-1]
|
||||
@@ -242,7 +246,7 @@ class Saml2EndSessionView(View, PrepareRequestMixin):
|
||||
return HttpResponseRedirect(logout_url)
|
||||
|
||||
|
||||
class Saml2AuthCallbackView(View, PrepareRequestMixin):
|
||||
class Saml2AuthCallbackView(View, PrepareRequestMixin, FlashMessageMixin):
|
||||
|
||||
def post(self, request):
|
||||
log_prompt = "Process SAML2 POST requests: {}"
|
||||
@@ -271,7 +275,13 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
|
||||
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
saml_user_data = self.get_attributes(saml_instance)
|
||||
user = auth.authenticate(request=request, saml_user_data=saml_user_data)
|
||||
try:
|
||||
user = auth.authenticate(request=request, saml_user_data=saml_user_data)
|
||||
except IntegrityError:
|
||||
title = _("SAML2 Error")
|
||||
msg = _('Please check if a user with the same username or email already exists')
|
||||
response = self.get_failed_response('/', title, msg)
|
||||
return response
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, reverse, render
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
@@ -116,23 +117,43 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
|
||||
|
||||
|
||||
class SessionCookieMiddleware(MiddlewareMixin):
|
||||
USER_LOGIN_ENCRYPTION_KEY_PAIR = 'user_login_encryption_key_pair'
|
||||
|
||||
@staticmethod
|
||||
def set_cookie_public_key(request, response):
|
||||
def set_cookie_public_key(self, request, response):
|
||||
if request.path.startswith('/api'):
|
||||
return
|
||||
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
||||
public_key = request.session.get(pub_key_name)
|
||||
cookie_key = request.COOKIES.get(pub_key_name)
|
||||
if public_key and public_key == cookie_key:
|
||||
|
||||
session_public_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
||||
session_private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
|
||||
session_public_key = request.session.get(session_public_key_name)
|
||||
cookie_public_key = request.COOKIES.get(session_public_key_name)
|
||||
|
||||
if session_public_key and session_public_key == cookie_public_key:
|
||||
return
|
||||
|
||||
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
private_key, public_key = gen_key_pair()
|
||||
private_key, public_key = self.get_key_pair()
|
||||
|
||||
public_key_decode = base64.b64encode(public_key.encode()).decode()
|
||||
request.session[pub_key_name] = public_key_decode
|
||||
request.session[pri_key_name] = private_key
|
||||
response.set_cookie(pub_key_name, public_key_decode)
|
||||
|
||||
request.session[session_public_key_name] = public_key_decode
|
||||
request.session[session_private_key_name] = private_key
|
||||
response.set_cookie(session_public_key_name, public_key_decode)
|
||||
|
||||
def get_key_pair(self):
|
||||
key_pair = cache.get(self.USER_LOGIN_ENCRYPTION_KEY_PAIR)
|
||||
if key_pair:
|
||||
return key_pair['private_key'], key_pair['public_key']
|
||||
|
||||
private_key, public_key = gen_key_pair()
|
||||
|
||||
key_pair = {
|
||||
'private_key': private_key,
|
||||
'public_key': public_key
|
||||
}
|
||||
cache.set(self.USER_LOGIN_ENCRYPTION_KEY_PAIR, key_pair, None)
|
||||
|
||||
return private_key, public_key
|
||||
|
||||
@staticmethod
|
||||
def set_cookie_session_prefix(request, response):
|
||||
|
||||
@@ -301,6 +301,7 @@ class MFAMixin:
|
||||
|
||||
|
||||
class AuthPostCheckMixin:
|
||||
|
||||
@classmethod
|
||||
def generate_reset_password_url_with_flash_msg(cls, user, message):
|
||||
reset_passwd_url = reverse('authentication:reset-password')
|
||||
@@ -319,20 +320,26 @@ class AuthPostCheckMixin:
|
||||
|
||||
@classmethod
|
||||
def _check_passwd_is_too_simple(cls, user: User, password):
|
||||
if password == 'admin' or password == 'ChangeMe':
|
||||
if not user.is_auth_backend_model():
|
||||
return
|
||||
if user.check_passwd_too_simple(password):
|
||||
message = _('Your password is too simple, please change it for security')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message=message)
|
||||
raise errors.PasswordTooSimple(url)
|
||||
|
||||
@classmethod
|
||||
def _check_passwd_need_update(cls, user: User):
|
||||
if user.need_update_password:
|
||||
if not user.is_auth_backend_model():
|
||||
return
|
||||
if user.check_need_update_password():
|
||||
message = _('You should to change your password before login')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
raise errors.PasswordNeedUpdate(url)
|
||||
|
||||
@classmethod
|
||||
def _check_password_require_reset_or_not(cls, user: User):
|
||||
if not user.is_auth_backend_model():
|
||||
return
|
||||
if user.password_has_expired:
|
||||
message = _('Your password has expired, please reset before logging in')
|
||||
url = cls.generate_reset_password_url_with_flash_msg(user, message)
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
#
|
||||
|
||||
from celery import shared_task
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
|
||||
@shared_task(verbose_name=_('Clean expired session'))
|
||||
|
||||
@shared_task(
|
||||
verbose_name=_('Clean expired session'),
|
||||
description=_(
|
||||
"Since user logins create sessions, the system will clean up expired sessions every 24 hours"
|
||||
)
|
||||
)
|
||||
@register_as_period_task(interval=3600 * 24)
|
||||
def clean_django_sessions():
|
||||
Session.objects.filter(expire_date__lt=timezone.now()).delete()
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
<p>
|
||||
<b>{% trans 'Username' %}:</b> {{ username }}<br>
|
||||
<b>{% trans 'Login Date' %}:</b> {{ time }}<br>
|
||||
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})
|
||||
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})<br>
|
||||
</p>
|
||||
|
||||
-
|
||||
<p>
|
||||
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
|
||||
</p>
|
||||
@@ -10,8 +10,7 @@
|
||||
{% trans 'Click here reset password' %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
-
|
||||
<br>
|
||||
<p>
|
||||
{% trans 'This link is valid for 1 hour. After it expires' %}
|
||||
<a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
{% trans 'Your password has just been successfully updated' %}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
|
||||
<b>{% trans 'Browser' %}:</b> {{ browser }}
|
||||
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
|
||||
<b>{% trans 'Browser' %}:</b> {{ browser }} <br>
|
||||
</p>
|
||||
-
|
||||
<p>
|
||||
{% trans 'If the password update was not initiated by you, your account may have security issues' %} <br/>
|
||||
{% trans 'If you have any questions, you can contact the administrator' %}
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
{% trans 'Your public key has just been successfully updated' %}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br/>
|
||||
<b>{% trans 'Browser' %}:</b> {{ browser }}
|
||||
<b>{% trans 'IP' %}:</b> {{ ip_address }} <br>
|
||||
<b>{% trans 'Browser' %}:</b> {{ browser }} <br>
|
||||
</p>
|
||||
-
|
||||
<p>
|
||||
{% trans 'If the public key update was not initiated by you, your account may have security issues' %} <br/>
|
||||
{% trans 'If you have any questions, you can contact the administrator' %}
|
||||
|
||||
@@ -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-$%^&*'):
|
||||
|
||||
@@ -38,9 +38,15 @@ class SuggestionMixin:
|
||||
class RenderToJsonMixin:
|
||||
@action(methods=[POST, PUT], detail=False, url_path='render-to-json')
|
||||
def render_to_json(self, request: Request, *args, **kwargs):
|
||||
rows = request.data
|
||||
if rows and isinstance(rows[0], dict):
|
||||
first = list(rows[0].values())[0]
|
||||
if first.startswith('#Help'):
|
||||
rows.pop(0)
|
||||
|
||||
data = {
|
||||
'title': (),
|
||||
'data': request.data,
|
||||
'data': rows,
|
||||
}
|
||||
|
||||
jms_context = getattr(request, 'jms_context', {})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.apps import AppConfig
|
||||
@@ -12,8 +13,11 @@ class CommonConfig(AppConfig):
|
||||
from . import signal_handlers # noqa
|
||||
from . import tasks # noqa
|
||||
from .signals import django_ready
|
||||
|
||||
excludes = ['migrate', 'compilemessages', 'makemigrations']
|
||||
for i in excludes:
|
||||
if i in sys.argv:
|
||||
return
|
||||
django_ready.send(CommonConfig)
|
||||
|
||||
if not os.environ.get('DJANGO_DEBUG_SHELL'):
|
||||
django_ready.send(CommonConfig)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
CRONTAB_AT_AM_TWO = '0 2 * * *'
|
||||
CRONTAB_AT_AM_THREE = '0 3 * * *'
|
||||
CRONTAB_AT_AM_TEN = '0 10 * * *'
|
||||
CRONTAB_AT_PM_TWO = '0 14 * * *'
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class BaseFileParser(BaseParser):
|
||||
if not matched:
|
||||
return v
|
||||
obj_name, obj_id = matched.groups()
|
||||
if len(obj_id) < 36:
|
||||
if obj_id.isdigit():
|
||||
obj_id = int(obj_id)
|
||||
return {'pk': obj_id, 'name': obj_name}
|
||||
|
||||
@@ -119,8 +119,6 @@ class BaseFileParser(BaseParser):
|
||||
value = field.to_file_internal_value(value)
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
value = value.lower() in ['true', '1', 'yes']
|
||||
elif isinstance(field, serializers.ChoiceField):
|
||||
value = value
|
||||
elif isinstance(field, ObjectRelatedField):
|
||||
if field.many:
|
||||
value = [self.id_name_to_obj(v) for v in value]
|
||||
@@ -164,6 +162,15 @@ class BaseFileParser(BaseParser):
|
||||
data.append(row_data)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def pop_help_text_if_need(rows):
|
||||
rows = list(rows)
|
||||
if not rows:
|
||||
return rows
|
||||
if rows[0][0].startswith('#Help'):
|
||||
rows.pop(0)
|
||||
return rows
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
assert parser_context is not None, '`parser_context` should not be `None`'
|
||||
|
||||
@@ -192,6 +199,7 @@ class BaseFileParser(BaseParser):
|
||||
request.jms_context = {}
|
||||
request.jms_context['column_title_field_pairs'] = column_title_field_pairs
|
||||
|
||||
rows = self.pop_help_text_if_need(rows)
|
||||
data = self.generate_data(field_names, rows)
|
||||
return data
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,12 +5,13 @@ from datetime import datetime
|
||||
|
||||
import pyzipper
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
from rest_framework.utils import encoders, json
|
||||
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from common.serializers import fields as common_fields
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -38,24 +39,27 @@ class BaseFileRenderer(BaseRenderer):
|
||||
filename_prefix = serializer.Meta.model.__name__.lower()
|
||||
else:
|
||||
filename_prefix = 'download'
|
||||
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
filename = "{}_{}.{}".format(filename_prefix, now, self.format)
|
||||
suffix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
if self.template == 'import':
|
||||
suffix = 'template'
|
||||
filename = "{}_{}.{}".format(filename_prefix, suffix, self.format)
|
||||
disposition = 'attachment; filename="{}"'.format(filename)
|
||||
response['Content-Disposition'] = disposition
|
||||
|
||||
def get_rendered_fields(self):
|
||||
fields = self.serializer.fields
|
||||
meta = getattr(self.serializer, 'Meta', None)
|
||||
if self.template == 'import':
|
||||
fields = [v for k, v in fields.items() if not v.read_only and k != "org_id" and k != 'id']
|
||||
fields_unimport = getattr(meta, 'fields_unimport_template', [])
|
||||
fields = [v for v in fields if v.field_name not in fields_unimport]
|
||||
elif self.template == 'update':
|
||||
fields = [v for k, v in fields.items() if not v.read_only and k != "org_id"]
|
||||
else:
|
||||
fields = [v for k, v in fields.items() if not v.write_only and k != "org_id"]
|
||||
|
||||
meta = getattr(self.serializer, 'Meta', None)
|
||||
if meta:
|
||||
fields_unexport = getattr(meta, 'fields_unexport', [])
|
||||
fields = [v for v in fields if v.field_name not in fields_unexport]
|
||||
fields_unexport = getattr(meta, 'fields_unexport', [])
|
||||
fields = [v for v in fields if v.field_name not in fields_unexport]
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
@@ -105,10 +109,10 @@ class BaseFileRenderer(BaseRenderer):
|
||||
value = field.to_file_representation(value)
|
||||
elif isinstance(value, bool):
|
||||
value = 'Yes' if value else 'No'
|
||||
elif isinstance(field, LabeledChoiceField):
|
||||
elif isinstance(field, common_fields.LabeledChoiceField):
|
||||
value = value or {}
|
||||
value = '{}({})'.format(value.get('label'), value.get('value'))
|
||||
elif isinstance(field, ObjectRelatedField):
|
||||
elif isinstance(field, common_fields.ObjectRelatedField):
|
||||
if field.many:
|
||||
value = [self.to_id_name(v) for v in value]
|
||||
else:
|
||||
@@ -126,6 +130,53 @@ class BaseFileRenderer(BaseRenderer):
|
||||
value = json.dumps(value, cls=encoders.JSONEncoder, ensure_ascii=False)
|
||||
return str(value)
|
||||
|
||||
def get_field_help_text(self, field):
|
||||
text = ''
|
||||
if hasattr(field, 'get_render_help_text'):
|
||||
text = field.get_render_help_text()
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
text = _('Yes/No')
|
||||
elif isinstance(field, serializers.CharField):
|
||||
if field.max_length:
|
||||
text = _('Text, max length {}').format(field.max_length)
|
||||
else:
|
||||
text = _("Long text, no length limit")
|
||||
elif isinstance(field, serializers.IntegerField):
|
||||
text = _('Number, min {} max {}').format(field.min_value, field.max_value)
|
||||
text = text.replace('min None', '').replace('max None', '')
|
||||
elif isinstance(field, serializers.DateTimeField):
|
||||
text = _('Datetime format {}').format(timezone.now().strftime(settings.REST_FRAMEWORK['DATETIME_FORMAT']))
|
||||
elif isinstance(field, serializers.IPAddressField):
|
||||
text = _('IP')
|
||||
elif isinstance(field, serializers.ChoiceField):
|
||||
choices = [str(v) for v in field.choices.keys()]
|
||||
if isinstance(field, common_fields.LabeledChoiceField):
|
||||
text = _("Choices, format name(value), name is optional for human read,"
|
||||
" value is requisite, options {}").format(','.join(choices))
|
||||
else:
|
||||
text = _("Choices, options {}").format(",".join(choices))
|
||||
elif isinstance(field, common_fields.PhoneField):
|
||||
text = _("Phone number, format +8612345678901")
|
||||
elif isinstance(field, common_fields.LabeledChoiceField):
|
||||
text = _('Label, format ["key:value"]')
|
||||
elif isinstance(field, common_fields.ObjectRelatedField):
|
||||
text = _("Object, format name(id), name is optional for human read, id is requisite")
|
||||
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
text = _('Object, format id')
|
||||
elif isinstance(field, serializers.ManyRelatedField):
|
||||
child_relation_class_name = field.child_relation.__class__.__name__
|
||||
if child_relation_class_name == "ObjectRelatedField":
|
||||
text = _('Objects, format ["name(id)", ...], name is optional for human read, id is requisite')
|
||||
elif child_relation_class_name == "LabelRelatedField":
|
||||
text = _('Labels, format ["key:value", ...], if label not exists, will create it')
|
||||
else:
|
||||
text = _('Objects, format ["id", ...]')
|
||||
elif isinstance(field, serializers.ListSerializer):
|
||||
child = field.child
|
||||
if hasattr(child, 'get_render_help_text'):
|
||||
text = child.get_render_help_text()
|
||||
return text
|
||||
|
||||
def generate_rows(self, data, render_fields):
|
||||
for item in data:
|
||||
row = []
|
||||
@@ -135,6 +186,17 @@ class BaseFileRenderer(BaseRenderer):
|
||||
row.append(value)
|
||||
yield row
|
||||
|
||||
def write_help_text_if_need(self):
|
||||
if self.template == 'export':
|
||||
return
|
||||
fields = self.get_rendered_fields()
|
||||
row = []
|
||||
for f in fields:
|
||||
text = self.get_field_help_text(f)
|
||||
row.append(text)
|
||||
row[0] = '#Help ' + str(row[0])
|
||||
self.write_row(row)
|
||||
|
||||
@abc.abstractmethod
|
||||
def initial_writer(self):
|
||||
raise NotImplementedError
|
||||
@@ -184,6 +246,7 @@ class BaseFileRenderer(BaseRenderer):
|
||||
rows = self.generate_rows(data, rendered_fields)
|
||||
self.initial_writer()
|
||||
self.write_column_titles(column_titles)
|
||||
self.write_help_text_if_need()
|
||||
self.write_rows(rows)
|
||||
self.after_render()
|
||||
value = self.get_rendered_value()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user