From 8e2c048f0c3d0716d120cbfb29c283118323e32c Mon Sep 17 00:00:00 2001 From: jiangweidong Date: Wed, 9 Nov 2022 18:23:00 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81Oracle=E3=80=81?= =?UTF-8?q?MongoDB=E3=80=81SQLServer=E6=95=B0=E6=8D=AE=E5=BA=93=E7=9A=84?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E8=84=9A=E6=9C=AC=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- .../change_secret/database/mongodb/main.yml | 43 ++ .../database/mongodb/manifest.yml | 6 + .../change_secret/database/oracle/main.yml | 45 ++ .../database/oracle/manifest.yml | 6 + .../change_secret/database/sqlserver/main.yml | 47 ++ .../database/sqlserver/manifest.yml | 6 + .../automations/change_secret/manager.py | 2 + .../gather_accounts/database/mongodb/main.yml | 22 + .../database/mongodb/manifest.yml | 6 + .../gather_accounts/database/mysql/main.yml | 4 +- .../gather_accounts/database/oracle/main.yml | 23 + .../database/oracle/manifest.yml | 6 + .../gather_facts/database/mongodb/main.yml | 22 + .../database/mongodb/manifest.yml | 6 + .../gather_facts/database/oracle/main.yml | 23 + .../gather_facts/database/oracle/manifest.yml | 6 + .../ping/database/mongodb/main.yml | 13 + .../ping/database/mongodb/manifest.yml | 6 + .../automations/ping/database/oracle/main.yml | 14 + .../ping/database/oracle/manifest.yml | 6 + .../ping/database/sqlserver/main.yml | 15 + .../ping/database/sqlserver/manifest.yml | 6 + .../push_account/database/mongodb/main.yml | 16 + .../database/mongodb/manifest.yml | 6 + .../push_account/database/oracle/main.yml | 16 + .../push_account/database/oracle/manifest.yml | 6 + .../verify_account/database/mongodb/main.yml | 13 + .../database/mongodb/manifest.yml | 6 + .../verify_account/database/oracle/main.yml | 14 + .../database/oracle/manifest.yml | 6 + .../database/sqlserver/main.yml | 15 + .../database/sqlserver/manifest.yml | 6 + apps/ops/ansible/inventory.py | 3 + apps/ops/ansible/modules/__init__.py | 0 apps/ops/ansible/modules/mongodb_ping.py | 126 ++++++ apps/ops/ansible/modules/mongodb_user.py | 426 ++++++++++++++++++ apps/ops/ansible/modules/oracle_info.py | 261 +++++++++++ apps/ops/ansible/modules/oracle_ping.py | 107 +++++ apps/ops/ansible/modules/oracle_user.py | 215 +++++++++ apps/ops/ansible/modules_utils/__init__.py | 0 .../ansible/modules_utils/oracle_common.py | 94 ++++ 42 files changed, 1668 insertions(+), 3 deletions(-) create mode 100644 apps/assets/automations/change_secret/database/mongodb/main.yml create mode 100644 apps/assets/automations/change_secret/database/mongodb/manifest.yml create mode 100644 apps/assets/automations/change_secret/database/oracle/main.yml create mode 100644 apps/assets/automations/change_secret/database/oracle/manifest.yml create mode 100644 apps/assets/automations/change_secret/database/sqlserver/main.yml create mode 100644 apps/assets/automations/change_secret/database/sqlserver/manifest.yml create mode 100644 apps/assets/automations/gather_accounts/database/mongodb/main.yml create mode 100644 apps/assets/automations/gather_accounts/database/mongodb/manifest.yml create mode 100644 apps/assets/automations/gather_accounts/database/oracle/main.yml create mode 100644 apps/assets/automations/gather_accounts/database/oracle/manifest.yml create mode 100644 apps/assets/automations/gather_facts/database/mongodb/main.yml create mode 100644 apps/assets/automations/gather_facts/database/mongodb/manifest.yml create mode 100644 apps/assets/automations/gather_facts/database/oracle/main.yml create mode 100644 apps/assets/automations/gather_facts/database/oracle/manifest.yml create mode 100644 apps/assets/automations/ping/database/mongodb/main.yml create mode 100644 apps/assets/automations/ping/database/mongodb/manifest.yml create mode 100644 apps/assets/automations/ping/database/oracle/main.yml create mode 100644 apps/assets/automations/ping/database/oracle/manifest.yml create mode 100644 apps/assets/automations/ping/database/sqlserver/main.yml create mode 100644 apps/assets/automations/ping/database/sqlserver/manifest.yml create mode 100644 apps/assets/automations/push_account/database/mongodb/main.yml create mode 100644 apps/assets/automations/push_account/database/mongodb/manifest.yml create mode 100644 apps/assets/automations/push_account/database/oracle/main.yml create mode 100644 apps/assets/automations/push_account/database/oracle/manifest.yml create mode 100644 apps/assets/automations/verify_account/database/mongodb/main.yml create mode 100644 apps/assets/automations/verify_account/database/mongodb/manifest.yml create mode 100644 apps/assets/automations/verify_account/database/oracle/main.yml create mode 100644 apps/assets/automations/verify_account/database/oracle/manifest.yml create mode 100644 apps/assets/automations/verify_account/database/sqlserver/main.yml create mode 100644 apps/assets/automations/verify_account/database/sqlserver/manifest.yml create mode 100644 apps/ops/ansible/modules/__init__.py create mode 100644 apps/ops/ansible/modules/mongodb_ping.py create mode 100644 apps/ops/ansible/modules/mongodb_user.py create mode 100644 apps/ops/ansible/modules/oracle_info.py create mode 100644 apps/ops/ansible/modules/oracle_ping.py create mode 100644 apps/ops/ansible/modules/oracle_user.py create mode 100644 apps/ops/ansible/modules_utils/__init__.py create mode 100644 apps/ops/ansible/modules_utils/oracle_common.py diff --git a/Dockerfile b/Dockerfile index 7e6eac422..9ff8dc4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,7 +76,7 @@ RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_ ARG VERSION ENV VERSION=$VERSION - +ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules ADD . . RUN cd utils \ && bash -ixeu build.sh \ diff --git a/apps/assets/automations/change_secret/database/mongodb/main.yml b/apps/assets/automations/change_secret/database/mongodb/main.yml new file mode 100644 index 000000000..02a568e0b --- /dev/null +++ b/apps/assets/automations/change_secret/database/mongodb/main.yml @@ -0,0 +1,43 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MongoDB connection + mongodb_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + register: db_info + + - name: Display MongoDB version + debug: + var: db_info.server_version + when: db_info is succeeded + + - name: Change MongoDB password + mongodb_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + db: "{{ jms_asset.specific.db_name }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + mongodb_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/mongodb/manifest.yml b/apps/assets/automations/change_secret/database/mongodb/manifest.yml new file mode 100644 index 000000000..a59c0033b --- /dev/null +++ b/apps/assets/automations/change_secret/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_mongodb +name: Change password for MongoDB +category: database +type: + - mongodb +method: change_secret diff --git a/apps/assets/automations/change_secret/database/oracle/main.yml b/apps/assets/automations/change_secret/database/oracle/main.yml new file mode 100644 index 000000000..c7b20a8db --- /dev/null +++ b/apps/assets/automations/change_secret/database/oracle/main.yml @@ -0,0 +1,45 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test Oracle connection + oracle_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" + register: db_info + + - name: Display Oracle version + debug: + var: db_info.server_version + when: db_info is succeeded + + - name: Change Oracle password + oracle_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" + when: db_info is succeeded + register: change_info + + - name: Verify password + oracle_ping: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ account.mode }}" + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/oracle/manifest.yml b/apps/assets/automations/change_secret/database/oracle/manifest.yml new file mode 100644 index 000000000..19f109ba6 --- /dev/null +++ b/apps/assets/automations/change_secret/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_oracle +name: Change password for Oracle +category: database +type: + - oracle +method: change_secret diff --git a/apps/assets/automations/change_secret/database/sqlserver/main.yml b/apps/assets/automations/change_secret/database/sqlserver/main.yml new file mode 100644 index 000000000..a617a1434 --- /dev/null +++ b/apps/assets/automations/change_secret/database/sqlserver/main.yml @@ -0,0 +1,47 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test SQLServer connection + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version + register: db_info + + - name: SQLServer version + set_fact: + info: + version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}" + - debug: + var: info + + - name: Change SQLServer password + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version" + when: db_info is succeeded + register: change_info + + - name: Verify password + community.general.mssql_script: + login_user: "{{ account.username }}" + login_password: "{{ account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version + when: + - db_info is succeeded + - change_info is succeeded diff --git a/apps/assets/automations/change_secret/database/sqlserver/manifest.yml b/apps/assets/automations/change_secret/database/sqlserver/manifest.yml new file mode 100644 index 000000000..799c9e623 --- /dev/null +++ b/apps/assets/automations/change_secret/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: change_secret_sqlserver +name: Change password for SQLServer +category: database +type: + - sqlserver +method: change_secret diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py index a8b7dd515..35ccba90a 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/assets/automations/change_secret/manager.py @@ -155,6 +155,8 @@ class ChangeSecretManager(BasePlaybookManager): 'secret': new_secret, 'private_key_path': private_key_path } + if asset.platform.type == 'oracle': + h['account']['mode'] = 'sysdba' if account.privileged else None inventory_hosts.append(h) method_hosts.append(h['name']) self.method_hosts_mapper[method_attr] = method_hosts diff --git a/apps/assets/automations/gather_accounts/database/mongodb/main.yml b/apps/assets/automations/gather_accounts/database/mongodb/main.yml new file mode 100644 index 000000000..fd7a296b7 --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/mongodb/main.yml @@ -0,0 +1,22 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + community.mongodb.mongodb_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + filter: users + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.users }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml b/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml new file mode 100644 index 000000000..a002848b4 --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: gather_accounts_mongodb +name: Gather account from MongoDB +category: database +type: + - mongodb +method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/database/mysql/main.yml b/apps/assets/automations/gather_accounts/database/mysql/main.yml index 4b166322a..cc934f20f 100644 --- a/apps/assets/automations/gather_accounts/database/mysql/main.yml +++ b/apps/assets/automations/gather_accounts/database/mysql/main.yml @@ -1,7 +1,7 @@ - hosts: mysql gather_facts: no vars: - ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python + ansible_python_interpreter: /usr/local/bin/python tasks: - name: Get info @@ -9,7 +9,7 @@ login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: 1234 + login_port: "{{ jms_asset.port }}" filter: users register: db_info diff --git a/apps/assets/automations/gather_accounts/database/oracle/main.yml b/apps/assets/automations/gather_accounts/database/oracle/main.yml new file mode 100644 index 000000000..a0e20ff7b --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/oracle/main.yml @@ -0,0 +1,23 @@ +- hosts: oralce + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + oracle_info: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" + filter: users + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.users }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_accounts/database/oracle/manifest.yml b/apps/assets/automations/gather_accounts/database/oracle/manifest.yml new file mode 100644 index 000000000..4753f1495 --- /dev/null +++ b/apps/assets/automations/gather_accounts/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: gather_accounts_oracle +name: Gather account from Oracle +category: database +type: + - oracle +method: gather_accounts diff --git a/apps/assets/automations/gather_facts/database/mongodb/main.yml b/apps/assets/automations/gather_facts/database/mongodb/main.yml new file mode 100644 index 000000000..37ce8bbd3 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/mongodb/main.yml @@ -0,0 +1,22 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + mongodb_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + register: db_info + + - name: Define info by set_fact + set_fact: + info: + version: "{{ db_info.server_version }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/database/mongodb/manifest.yml b/apps/assets/automations/gather_facts/database/mongodb/manifest.yml new file mode 100644 index 000000000..9b832e838 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: gather_facts_mongodb +name: Gather facts from MongoDB +category: database +type: + - mongodb +method: gather_facts diff --git a/apps/assets/automations/gather_facts/database/oracle/main.yml b/apps/assets/automations/gather_facts/database/oracle/main.yml new file mode 100644 index 000000000..21ab639a4 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/oracle/main.yml @@ -0,0 +1,23 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Get info + oracle_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" + register: db_info + + - name: Define info by set_fact + set_fact: + info: + version: "{{ db_info.server_version }}" + + - debug: + var: info diff --git a/apps/assets/automations/gather_facts/database/oracle/manifest.yml b/apps/assets/automations/gather_facts/database/oracle/manifest.yml new file mode 100644 index 000000000..d350579d6 --- /dev/null +++ b/apps/assets/automations/gather_facts/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: gather_facts_oracle +name: Gather facts from Oracle +category: database +type: + - oracle +method: gather_facts diff --git a/apps/assets/automations/ping/database/mongodb/main.yml b/apps/assets/automations/ping/database/mongodb/main.yml new file mode 100644 index 000000000..867c51ace --- /dev/null +++ b/apps/assets/automations/ping/database/mongodb/main.yml @@ -0,0 +1,13 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test MongoDB connection + mongodb_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" diff --git a/apps/assets/automations/ping/database/mongodb/manifest.yml b/apps/assets/automations/ping/database/mongodb/manifest.yml new file mode 100644 index 000000000..45b90eb72 --- /dev/null +++ b/apps/assets/automations/ping/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: mongodb_ping +name: Ping MongoDB +category: database +type: + - mongodb +method: ping diff --git a/apps/assets/automations/ping/database/oracle/main.yml b/apps/assets/automations/ping/database/oracle/main.yml new file mode 100644 index 000000000..fefad7148 --- /dev/null +++ b/apps/assets/automations/ping/database/oracle/main.yml @@ -0,0 +1,14 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test Oracle connection + oracle_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" diff --git a/apps/assets/automations/ping/database/oracle/manifest.yml b/apps/assets/automations/ping/database/oracle/manifest.yml new file mode 100644 index 000000000..3912941c3 --- /dev/null +++ b/apps/assets/automations/ping/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: oracle_ping +name: Ping Oracle +category: database +type: + - oracle +method: ping diff --git a/apps/assets/automations/ping/database/sqlserver/main.yml b/apps/assets/automations/ping/database/sqlserver/main.yml new file mode 100644 index 000000000..839a785a5 --- /dev/null +++ b/apps/assets/automations/ping/database/sqlserver/main.yml @@ -0,0 +1,15 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Test SQLServer connection + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version diff --git a/apps/assets/automations/ping/database/sqlserver/manifest.yml b/apps/assets/automations/ping/database/sqlserver/manifest.yml new file mode 100644 index 000000000..b5cd6bf88 --- /dev/null +++ b/apps/assets/automations/ping/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: sqlserver_ping +name: Ping SQLServer +category: database +type: + - sqlserver +method: ping diff --git a/apps/assets/automations/push_account/database/mongodb/main.yml b/apps/assets/automations/push_account/database/mongodb/main.yml new file mode 100644 index 000000000..d516251db --- /dev/null +++ b/apps/assets/automations/push_account/database/mongodb/main.yml @@ -0,0 +1,16 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Add user account.username + mongodb_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + db: "{{ jms_asset.specific.db_name }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" diff --git a/apps/assets/automations/push_account/database/mongodb/manifest.yml b/apps/assets/automations/push_account/database/mongodb/manifest.yml new file mode 100644 index 000000000..9de93f5e7 --- /dev/null +++ b/apps/assets/automations/push_account/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_mongodb +name: Push account from MongoDB +category: database +type: + - mongodb +method: push_account diff --git a/apps/assets/automations/push_account/database/oracle/main.yml b/apps/assets/automations/push_account/database/oracle/main.yml new file mode 100644 index 000000000..5812dfee1 --- /dev/null +++ b/apps/assets/automations/push_account/database/oracle/main.yml @@ -0,0 +1,16 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Add user account.username + oracle_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" + name: "{{ account.username }}" + password: "{{ account.secret }}" diff --git a/apps/assets/automations/push_account/database/oracle/manifest.yml b/apps/assets/automations/push_account/database/oracle/manifest.yml new file mode 100644 index 000000000..da1faed6f --- /dev/null +++ b/apps/assets/automations/push_account/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: push_account_oracle +name: Push account from Oracle +category: database +type: + - oracle +method: push_account diff --git a/apps/assets/automations/verify_account/database/mongodb/main.yml b/apps/assets/automations/verify_account/database/mongodb/main.yml new file mode 100644 index 000000000..1cf79b694 --- /dev/null +++ b/apps/assets/automations/verify_account/database/mongodb/main.yml @@ -0,0 +1,13 @@ +- hosts: mongdb + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + mongodb_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" diff --git a/apps/assets/automations/verify_account/database/mongodb/manifest.yml b/apps/assets/automations/verify_account/database/mongodb/manifest.yml new file mode 100644 index 000000000..24cc398c2 --- /dev/null +++ b/apps/assets/automations/verify_account/database/mongodb/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_mongodb +name: Verify account from MongoDB +category: database +type: + - mongodb +method: verify_account diff --git a/apps/assets/automations/verify_account/database/oracle/main.yml b/apps/assets/automations/verify_account/database/oracle/main.yml new file mode 100644 index 000000000..ed4091401 --- /dev/null +++ b/apps/assets/automations/verify_account/database/oracle/main.yml @@ -0,0 +1,14 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + oracle_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.specific.db_name }}" + mode: "{{ jms_account.mode }}" diff --git a/apps/assets/automations/verify_account/database/oracle/manifest.yml b/apps/assets/automations/verify_account/database/oracle/manifest.yml new file mode 100644 index 000000000..b58a7f888 --- /dev/null +++ b/apps/assets/automations/verify_account/database/oracle/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_oracle +name: Verify account from Oracle +category: database +type: + - oracle +method: verify_account diff --git a/apps/assets/automations/verify_account/database/sqlserver/main.yml b/apps/assets/automations/verify_account/database/sqlserver/main.yml new file mode 100644 index 000000000..256803702 --- /dev/null +++ b/apps/assets/automations/verify_account/database/sqlserver/main.yml @@ -0,0 +1,15 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /usr/local/bin/python + + tasks: + - name: Verify account + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.specific.db_name }}' + script: | + SELECT @@version diff --git a/apps/assets/automations/verify_account/database/sqlserver/manifest.yml b/apps/assets/automations/verify_account/database/sqlserver/manifest.yml new file mode 100644 index 000000000..8af52ab0b --- /dev/null +++ b/apps/assets/automations/verify_account/database/sqlserver/manifest.yml @@ -0,0 +1,6 @@ +id: verify_account_sqlserver +name: Verify account from SQLServer +category: database +type: + - sqlserver +method: verify_account diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 1e006ae77..f71801b76 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -112,6 +112,9 @@ class JMSInventory: 'secret': account.secret, 'secret_type': account.secret_type } if account else None } + if host['jms_account'] and asset.platform.type == 'oracle': + host['jms_account']['mode'] = 'sysdba' if account.privileged else None + ansible_config = dict(automation.ansible_config) ansible_connection = ansible_config.get('ansible_connection', 'ssh') host.update(ansible_config) diff --git a/apps/ops/ansible/modules/__init__.py b/apps/ops/ansible/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ops/ansible/modules/mongodb_ping.py b/apps/ops/ansible/modules/mongodb_ping.py new file mode 100644 index 000000000..d018cd852 --- /dev/null +++ b/apps/ops/ansible/modules/mongodb_ping.py @@ -0,0 +1,126 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: mongodb_ping +short_description: Check remote MongoDB server availability +description: +- Simple module to check remote MongoDB server availability. + +requirements: + - "pymongo" +''' + +EXAMPLES = ''' +- name: > + Ping MongoDB server using non-default credentials and SSL + registering the return values into the result variable for future use + mongodb_ping: + login_db: test_db + login_host: jumpserver + login_user: jms + login_password: secret_pass + ssl: True + ssl_ca_certs: "/tmp/ca.crt" + ssl_certfile: "/tmp/tls.key" #cert and key in one file + connection_options: + - "tlsAllowInvalidHostnames=true" +''' + +RETURN = ''' +is_available: + description: MongoDB server availability. + returned: always + type: bool + sample: true +server_version: + description: MongoDB server version. + returned: always + type: str + sample: '4.0.0' +conn_err_msg: + description: Connection error message. + returned: always + type: str + sample: '' +''' + + +from pymongo.errors import PyMongoError +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import ( + mongodb_common_argument_spec, + mongo_auth, + get_mongodb_client, +) + + +class MongoDBPing(object): + def __init__(self, module, client): + self.module = module + self.client = client + self.is_available = False + self.conn_err_msg = '' + self.version = '' + + def do(self): + self.get_mongodb_version() + return self.is_available, self.version + + def get_err(self): + return self.conn_err_msg + + def get_mongodb_version(self): + try: + server_info = self.client.server_info() + self.is_available = True + self.version = server_info.get('version', '') + except PyMongoError as err: + self.is_available = False + self.version = '' + self.conn_err_msg = err + + +# ========================================= +# Module execution. +# + + +def main(): + argument_spec = mongodb_common_argument_spec() + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + client = None + result = { + 'changed': False, 'is_available': False, 'server_version': '' + } + try: + client = get_mongodb_client(module, directConnection=True) + client = mongo_auth(module, client, directConnection=True) + except Exception as e: + module.fail_json(msg='Unable to connect to database: %s' % to_native(e)) + + mongodb_ping = MongoDBPing(module, client) + result["is_available"], result["server_version"] = mongodb_ping.do() + conn_err_msg = mongodb_ping.get_err() + if conn_err_msg: + module.fail_json(msg='Unable to connect to database: %s' % conn_err_msg) + + try: + client.close() + except Exception: + pass + + return module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/mongodb_user.py b/apps/ops/ansible/modules/mongodb_user.py new file mode 100644 index 000000000..93493fd58 --- /dev/null +++ b/apps/ops/ansible/modules/mongodb_user.py @@ -0,0 +1,426 @@ +#!/usr/bin/python + +# Modified from ansible_collections.community.mongodb.plugins.modules.mongodb_user + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: mongodb_user +short_description: Adds or removes a user from a MongoDB database +description: + - Adds or removes a user from a MongoDB database. +version_added: "1.0.0" + +extends_documentation_fragment: + - community.mongodb.login_options + - community.mongodb.ssl_options + +options: + replica_set: + description: + - Replica set to connect to (automatically connects to primary for writes). + type: str + database: + description: + - The name of the database to add/remove the user from. + required: true + type: str + aliases: [db] + name: + description: + - The name of the user to add or remove. + required: true + aliases: [user] + type: str + password: + description: + - The password to use for the user. + type: str + aliases: [pass] + roles: + type: list + elements: raw + description: + - > + The database user roles valid values could either be one or more of the following strings: + 'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase', + 'dbAdminAnyDatabase' + - "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'." + - "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required." + state: + description: + - The database user state. + default: present + choices: [absent, present] + type: str + update_password: + default: always + choices: [always, on_create] + description: + - C(always) will always update passwords and cause the module to return changed. + - C(on_create) will only set the password for newly created users. + - This must be C(always) to use the localhost exception when adding the first admin user. + - This option is effectively ignored when using x.509 certs. It is defaulted to 'on_create' to maintain a \ + a specific module behaviour when the login_database is '$external'. + type: str + create_for_localhost_exception: + type: path + description: + - This is parmeter is only useful for handling special treatment around the localhost exception. + - If C(login_user) is defined, then the localhost exception is not active and this parameter has no effect. + - If this file is NOT present (and C(login_user) is not defined), then touch this file after successfully adding the user. + - If this file is present (and C(login_user) is not defined), then skip this task. + +notes: + - Requires the pymongo Python package on the remote host, version 2.4.2+. This + can be installed using pip or the OS package manager. Newer mongo server versions require newer + pymongo versions. @see http://api.mongodb.org/python/current/installation.html +requirements: + - "pymongo" +author: + - "Elliott Foster (@elliotttf)" + - "Julien Thebault (@Lujeni)" +''' + +EXAMPLES = ''' +- name: Create 'burgers' database user with name 'bob' and password '12345'. + community.mongodb.mongodb_user: + database: burgers + name: bob + password: 12345 + state: present + +- name: Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly) + community.mongodb.mongodb_user: + database: burgers + name: bob + password: 12345 + state: present + ssl: True + +- name: Delete 'burgers' database user with name 'bob'. + community.mongodb.mongodb_user: + database: burgers + name: bob + state: absent + +- name: Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style) + community.mongodb.mongodb_user: + database: burgers + name: ben + password: 12345 + roles: read + state: present + +- name: Define roles + community.mongodb.mongodb_user: + database: burgers + name: jim + password: 12345 + roles: readWrite,dbAdmin,userAdmin + state: present + +- name: Define roles + community.mongodb.mongodb_user: + database: burgers + name: joe + password: 12345 + roles: readWriteAnyDatabase + state: present + +- name: Add a user to database in a replica set, the primary server is automatically discovered and written to + community.mongodb.mongodb_user: + database: burgers + name: bob + replica_set: belcher + password: 12345 + roles: readWriteAnyDatabase + state: present + +# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is useful for oplog access (MONGO_OPLOG_URL). +# please notice the credentials must be added to the 'admin' database because the 'local' database is not synchronized and can't receive user credentials +# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin" +# This syntax requires mongodb 2.6+ and pymongo 2.5+ +- name: Roles as a dictionary + community.mongodb.mongodb_user: + login_user: root + login_password: root_password + database: admin + user: oplog_reader + password: oplog_reader_password + state: present + replica_set: belcher + roles: + - db: local + role: read + +- name: Adding a user with X.509 Member Authentication + community.mongodb.mongodb_user: + login_host: "mongodb-host.test" + login_port: 27001 + login_database: "$external" + database: "admin" + name: "admin" + password: "test" + roles: + - dbAdminAnyDatabase + ssl: true + ssl_ca_certs: "/tmp/ca.crt" + ssl_certfile: "/tmp/tls.key" #cert and key in one file + state: present + auth_mechanism: "MONGODB-X509" + connection_options: + - "tlsAllowInvalidHostnames=true" +''' + +RETURN = ''' +user: + description: The name of the user to add or remove. + returned: success + type: str +''' + +import os +import traceback +from operator import itemgetter + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import binary_type, text_type +from ansible.module_utils._text import to_native, to_bytes +from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import ( + missing_required_lib, + mongodb_common_argument_spec, + mongo_auth, + PYMONGO_IMP_ERR, + pymongo_found, + get_mongodb_client, +) + + +def user_find(client, user, db_name): + """Check if the user exists. + + Args: + client (cursor): Mongodb cursor on admin database. + user (str): User to check. + db_name (str): User's database. + + Returns: + dict: when user exists, False otherwise. + """ + try: + for mongo_user in client[db_name].command('usersInfo')['users']: + if mongo_user['user'] == user: + # NOTE: there is no 'db' field in mongo 2.4. + if 'db' not in mongo_user: + return mongo_user + # Workaround to make the condition works with AWS DocumentDB, + # since all users are in the admin database. + if mongo_user["db"] in [db_name, "admin"]: + return mongo_user + except Exception as excep: + if hasattr(excep, 'code') and excep.code == 11: # 11=UserNotFound + pass # Allow return False + else: + raise + return False + + +def user_add(module, client, db_name, user, password, roles): + # pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated + # without reproducing a lot of the logic in database.py of pymongo + db = client[db_name] + + try: + exists = user_find(client, user, db_name) + except Exception as excep: + # We get this exception: "not authorized on admin to execute command" + # when auth is enabled on a new instance. The loalhost exception should + # allow us to create the first user. If the localhost exception does not apply, + # then user creation will also fail with unauthorized. So, ignore Unauthorized here. + if hasattr(excep, 'code') and excep.code == 13: # 13=Unauthorized + exists = False + else: + raise + + if exists: + user_add_db_command = 'updateUser' + if not roles: + roles = None + else: + user_add_db_command = 'createUser' + + user_dict = {} + + if password is not None: + user_dict["pwd"] = password + if roles is not None: + user_dict["roles"] = roles + + db.command(user_add_db_command, user, **user_dict) + + +def user_remove(module, client, db_name, user): + exists = user_find(client, user, db_name) + if exists: + if module.check_mode: + module.exit_json(changed=True, user=user) + db = client[db_name] + db.command("dropUser", user) + else: + module.exit_json(changed=False, user=user) + + +def check_if_roles_changed(uinfo, roles, db_name): + # We must be aware of users which can read the oplog on a replicaset + # Such users must have access to the local DB, but since this DB does not store users credentials + # and is not synchronized among replica sets, the user must be stored on the admin db + # Therefore their structure is the following : + # { + # "_id" : "admin.oplog_reader", + # "user" : "oplog_reader", + # "db" : "admin", # <-- admin DB + # "roles" : [ + # { + # "role" : "read", + # "db" : "local" # <-- local DB + # } + # ] + # } + + def make_sure_roles_are_a_list_of_dict(roles, db_name): + output = list() + for role in roles: + if isinstance(role, (binary_type, text_type)): + new_role = {"role": role, "db": db_name} + output.append(new_role) + else: + output.append(role) + return output + + roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name) + uinfo_roles = uinfo.get('roles', []) + + if sorted(roles_as_list_of_dict, key=itemgetter('db')) == sorted(uinfo_roles, key=itemgetter('db')): + return False + return True + + +# ========================================= +# Module execution. +# + +def main(): + argument_spec = mongodb_common_argument_spec() + argument_spec.update( + database=dict(required=True, aliases=['db']), + name=dict(required=True, aliases=['user']), + password=dict(aliases=['pass'], no_log=True), + replica_set=dict(default=None), + roles=dict(default=None, type='list', elements='raw'), + state=dict(default='present', choices=['absent', 'present']), + update_password=dict(default="always", choices=["always", "on_create"], no_log=False), + create_for_localhost_exception=dict(default=None, type='path'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + login_user = module.params['login_user'] + + # Certs don't have a password but we want this module behaviour + if module.params['login_database'] == '$external': + module.params['update_password'] = 'on_create' + + if not pymongo_found: + module.fail_json(msg=missing_required_lib('pymongo'), + exception=PYMONGO_IMP_ERR) + + create_for_localhost_exception = module.params['create_for_localhost_exception'] + b_create_for_localhost_exception = ( + to_bytes(create_for_localhost_exception, errors='surrogate_or_strict') + if create_for_localhost_exception is not None else None + ) + + db_name = module.params['database'] + user = module.params['name'] + password = module.params['password'] + roles = module.params['roles'] or [] + state = module.params['state'] + update_password = module.params['update_password'] + + try: + directConnection = False + if module.params['replica_set'] is None: + directConnection = True + client = get_mongodb_client(module, directConnection=directConnection) + client = mongo_auth(module, client, directConnection=directConnection) + except Exception as e: + module.fail_json(msg='Unable to connect to database: %s' % to_native(e)) + + if state == 'present': + if password is None and update_password == 'always': + module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') + + if login_user is None and create_for_localhost_exception is not None: + if os.path.exists(b_create_for_localhost_exception): + try: + client.close() + except Exception: + pass + module.exit_json(changed=False, user=user, skipped=True, msg="The path in create_for_localhost_exception exists.") + + try: + if update_password != 'always': + uinfo = user_find(client, user, db_name) + if uinfo: + password = None + if not check_if_roles_changed(uinfo, roles, db_name): + module.exit_json(changed=False, user=user) + + if module.check_mode: + module.exit_json(changed=True, user=user) + user_add(module, client, db_name, user, password, roles) + except Exception as e: + module.fail_json(msg='Unable to add or update user: %s' % to_native(e), exception=traceback.format_exc()) + finally: + try: + client.close() + except Exception: + pass + # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848 + # newuinfo = user_find(client, user, db_name) + # if uinfo['role'] == newuinfo['role'] and CheckPasswordHere: + # module.exit_json(changed=False, user=user) + + if login_user is None and create_for_localhost_exception is not None: + # localhost exception applied. + try: + # touch the file + open(b_create_for_localhost_exception, 'wb').close() + except Exception as e: + module.fail_json( + changed=True, + msg='Added user but unable to touch create_for_localhost_exception file %s: %s' % (create_for_localhost_exception, to_native(e)), + exception=traceback.format_exc() + ) + + elif state == 'absent': + try: + user_remove(module, client, db_name, user) + except Exception as e: + module.fail_json(msg='Unable to remove user: %s' % to_native(e), exception=traceback.format_exc()) + finally: + try: + client.close() + except Exception: + pass + module.exit_json(changed=True, user=user) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/oracle_info.py b/apps/ops/ansible/modules/oracle_info.py new file mode 100644 index 000000000..005206be9 --- /dev/null +++ b/apps/ops/ansible/modules/oracle_info.py @@ -0,0 +1,261 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: oracle_info +short_description: Gather information about Oracle servers +description: +- Gathers information about Oracle servers. + +options: + filter: + description: + - Limit the collected information by comma separated string or YAML list. + - Allowable values are C(version), C(databases), C(settings), C(users). + - By default, collects all subsets. + - You can use '!' before value (for example, C(!users)) to exclude it from the information. + - If you pass including and excluding values to the filter, for example, I(filter=!settings,version), + the excluding values, C(!settings) in this case, will be ignored. + type: list + elements: str + login_db: + description: + - Database name to connect to. + - It makes sense if I(login_user) is allowed to connect to a specific database only. + type: str + exclude_fields: + description: + - List of fields which are not needed to collect. + - "Supports elements: C(db_size). Unsupported elements will be ignored." + type: list + elements: str +''' + +EXAMPLES = r''' +- name: Get Oracle version with non-default credentials + oracle_info: + login_user: mysuperuser + login_password: mysuperpass + login_database: service_name + filter: version + +- name: Collect all info except settings and users by sys + oracle_info: + login_user: sys + login_password: sys_pass + login_database: service_name + filter: "!settings,!users" + exclude_fields: db_size +''' + +RETURN = r''' +version: + description: Database server version. + returned: if not excluded by filter + type: dict + sample: { "version": {"full": "11.2.0.1.0"} } + contains: + full: + description: Full server version. + returned: if not excluded by filter + type: str + sample: "11.2.0.1.0" +databases: + description: Information about databases. + returned: if not excluded by filter + type: dict + sample: + - { "USERS": { "size": 5242880 }, "EXAMPLE": { "size": 104857600 } } + contains: + size: + description: Database size in bytes. + returned: if not excluded by filter + type: dict + sample: { 'size': 656594 } +settings: + description: Global settings (variables) information. + returned: if not excluded by filter + type: dict + sample: + - { "result_cache_mode": "MANUAL", "instance_type": "RDBMS" } +users: + description: Users information. + returned: if not excluded by filter + type: dict + sample: + - { "USERS": { "TEST": { "USERNAME": "TEST", "ACCOUNT_STATUS": "OPEN" } } } +''' + +from ansible.module_utils.basic import AnsibleModule + +from ops.ansible.modules_utils.oracle_common import ( + OracleClient, oracle_common_argument_spec +) + + +class OracleInfo(object): + def __init__(self, module, oracle_client): + self.module = module + self.oracle_client = oracle_client + self.info = { + 'version': {}, 'databases': {}, + 'settings': {}, 'users': {}, + } + + def get_info(self, filter_, exclude_fields): + include_list = [] + exclude_list = [] + + if filter_: + partial_info = {} + + for fi in filter_: + if fi.lstrip('!') not in self.info: + self.module.warn('filter element: %s is not allowable, ignored' % fi) + continue + + if fi[0] == '!': + exclude_list.append(fi.lstrip('!')) + else: + include_list.append(fi) + + if include_list: + self.__collect(exclude_fields, set(include_list)) + + for i in self.info: + if i in include_list: + partial_info[i] = self.info[i] + else: + not_in_exclude_list = list(set(self.info) - set(exclude_list)) + self.__collect(exclude_fields, set(not_in_exclude_list)) + + for i in self.info: + if i not in exclude_list: + partial_info[i] = self.info[i] + return partial_info + else: + self.__collect(exclude_fields, set(self.info)) + return self.info + + def __collect(self, exclude_fields, wanted): + """Collect all possible subsets.""" + if 'version' in wanted: + self.__get_version() + + if 'settings' in wanted: + self.__get_settings() + + if 'databases' in wanted: + self.__get_databases(exclude_fields) + # + if 'users' in wanted: + self.__get_users() + + def __get_version(self): + version_sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION where ROWNUM=1' + rtn, err = self.oracle_client.execute(version_sql, exception_to_fail=True) + self.info['version'] = {'full': rtn.get('version')} + + def __get_settings(self): + """Get global variables (instance settings).""" + def _set_settings_value(item_dict): + try: + self.info['settings'][item_dict['name']] = item_dict['value'] + except KeyError: + pass + + settings_sql = "SELECT name, value FROM V$PARAMETER" + rtn, err = self.oracle_client.execute(settings_sql, exception_to_fail=True) + + if isinstance(rtn, dict): + _set_settings_value(rtn) + elif isinstance(rtn, list): + for i in rtn: + _set_settings_value(i) + + def __get_users(self): + """Get user info.""" + def _set_users_value(item_dict): + try: + tablespace = item_dict.pop('default_tablespace') + username = item_dict.pop('username') + partial_users = self.info['users'].get(tablespace, {}) + partial_users[username] = item_dict + self.info['users'][tablespace] = partial_users + except KeyError: + pass + + users_sql = "SELECT * FROM dba_users" + rtn, err = self.oracle_client.execute(users_sql, exception_to_fail=True) + if isinstance(rtn, dict): + _set_users_value(rtn) + elif isinstance(rtn, list): + for i in rtn: + _set_users_value(i) + + def __get_databases(self, exclude_fields): + """Get info about databases.""" + def _set_databases_value(item_dict): + try: + tablespace_name = item_dict.pop('tablespace_name') + size = item_dict.get('size') + partial_params = {} + if size: + partial_params['size'] = size + self.info['databases'][tablespace_name] = partial_params + except KeyError: + pass + + database_sql = 'SELECT ' \ + ' tablespace_name, sum(bytes) as "size"' \ + 'FROM dba_data_files GROUP BY tablespace_name' + if exclude_fields and 'db_size' in exclude_fields: + database_sql = "SELECT " \ + " tablespace_name " \ + "FROM dba_data_files GROUP BY tablespace_name" + + rtn, err = self.oracle_client.execute(database_sql, exception_to_fail=True) + if isinstance(rtn, dict): + _set_databases_value(rtn) + elif isinstance(rtn, list): + for i in rtn: + _set_databases_value(i) + + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = oracle_common_argument_spec() + argument_spec.update( + filter=dict(type='list'), + exclude_fields=dict(type='list'), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + filter_ = module.params['filter'] + exclude_fields = module.params['exclude_fields'] + + if filter_: + filter_ = [f.strip() for f in filter_] + + if exclude_fields: + exclude_fields = set([f.strip() for f in exclude_fields]) + + oracle_client = OracleClient(module) + oracle = OracleInfo(module, oracle_client) + + module.exit_json(changed=False, **oracle.get_info(filter_, exclude_fields)) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/oracle_ping.py b/apps/ops/ansible/modules/oracle_ping.py new file mode 100644 index 000000000..df1069d11 --- /dev/null +++ b/apps/ops/ansible/modules/oracle_ping.py @@ -0,0 +1,107 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: oracle_ping +short_description: Check remote Oracle server availability +description: +- Simple module to check remote Oracle server availability. + +requirements: + - "oracledb" +''' + +EXAMPLES = ''' +- name: > + Ping Oracle server using non-default credentials and SSL + registering the return values into the result variable for future use + oracle_ping: + login_host: jumpserver + login_port: 1521 + login_user: jms + login_password: secret_pass + login_database: test_db +''' + +RETURN = ''' +is_available: + description: Oracle server availability. + returned: always + type: bool + sample: true +server_version: + description: Oracle server version. + returned: always + type: str + sample: '4.0.0' +conn_err_msg: + description: Connection error message. + returned: always + type: str + sample: '' +''' + +from ansible.module_utils.basic import AnsibleModule +from ops.ansible.modules_utils.oracle_common import ( + OracleClient, oracle_common_argument_spec +) + + +class OracleDBPing(object): + def __init__(self, module, oracle_client): + self.module = module + self.oracle_client = oracle_client + self.is_available = False + self.conn_err_msg = '' + self.version = '' + + def do(self): + self.get_oracle_version() + return self.is_available, self.version + + def get_err(self): + return self.conn_err_msg + + def get_oracle_version(self): + version_sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION where ROWNUM=1' + rtn, err = self.oracle_client.execute(version_sql) + if err: + self.conn_err_msg = err + else: + self.version = rtn.get('version') + self.is_available = True + + +# ========================================= +# Module execution. +# + + +def main(): + argument_spec = oracle_common_argument_spec() + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + result = { + 'changed': False, 'is_available': False, 'server_version': '' + } + oracle_client = OracleClient(module) + + oracle_ping = OracleDBPing(module, oracle_client) + result["is_available"], result["server_version"] = oracle_ping.do() + conn_err_msg = oracle_ping.get_err() + oracle_client.close() + if conn_err_msg: + module.fail_json(msg='Unable to connect to database: %s' % conn_err_msg) + + return module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules/oracle_user.py b/apps/ops/ansible/modules/oracle_user.py new file mode 100644 index 000000000..9e23fe70c --- /dev/null +++ b/apps/ops/ansible/modules/oracle_user.py @@ -0,0 +1,215 @@ +#!/usr/bin/python + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: oracle_user +short_description: Adds or removes a user from a Oracle database +description: + - Adds or removes a user from a Oracle database. + +options: + authentication_type: + description: + - Authentication type of the user(default password) + required: false + type: str + choices: ['external', 'global', 'no_authentication', 'password'] + default_tablespace: + description: + - The default tablespace for the user + - If not provided, the default is used + required: false + type: str + oracle_home: + description: + - Define the directory into which all Oracle software is installed. + - Define ORACLE_HOME environment variable if set. + type: str + state: + description: + - The database user state. + default: present + choices: [absent, present] + type: str + update_password: + default: always + choices: [always, on_create] + description: + - C(always) will always update passwords and cause the module to return changed. + - C(on_create) will only set the password for newly created users. + type: str + temporary_tablespace: + description: + - The default temporary tablespace for the user + - If not provided, the default is used + required: false + type: str + name: + description: + - The name of the user to add or remove. + required: true + aliases: [user] + type: str + password: + description: + - The password to use for the user. + type: str + aliases: [pass] + +requirements: + - "oracledb" +''' + +EXAMPLES = ''' +- name: Create default tablespace user with name 'jms' and password '123456'. + oracle_user: + hostname: "remote server" + login_database: "helowin" + login_username: "system" + login_password: "123456" + name: "jms" + password: "123456" + +- name: Delete user with name 'jms'. + oracle_user: + hostname: "remote server" + login_database: "helowin" + login_username: "system" + login_password: "123456" + name: "jms" + state: "absent" +''' + +RETURN = ''' +name: + description: The name of the user to add or remove. + returned: success + type: str +''' + +from ansible.module_utils.basic import AnsibleModule + +from ops.ansible.modules_utils.oracle_common import ( + OracleClient, oracle_common_argument_spec +) + + +def user_find(oracle_client, username): + user = None + username = username.upper() + user_find_sql = "select username, " \ + " authentication_type, " \ + " default_tablespace, " \ + " temporary_tablespace " \ + "from dba_users where username='%s'" % username + rtn, err = oracle_client.execute(user_find_sql) + if isinstance(rtn, dict): + user = rtn + return user + + +def user_add( + module, oracle_client, username, password, auth_type, + default_tablespace, temporary_tablespace +): + username = username.upper() + extend_sql = None + user = user_find(oracle_client, username) + auth_type = auth_type.lower() + identified_suffix_map = { + 'external': 'identified externally ', + 'global': 'identified globally ', + 'password': 'identified by "%s" ', + } + if user: + user_sql = "alter user %s " % username + user_sql += identified_suffix_map.get(auth_type, 'no authentication ') % password + + if default_tablespace and default_tablespace.lower() != user['default_tablespace'].lower(): + user_sql += 'default tablespace %s quota unlimited on %s ' % (default_tablespace, default_tablespace) + if temporary_tablespace and temporary_tablespace.lower() != user['temporary_tablespace'].lower(): + user_sql += 'temporary tablespace %s ' % temporary_tablespace + else: + user_sql = "create user %s " % username + user_sql += identified_suffix_map.get(auth_type, 'no authentication ') % password + if default_tablespace: + user_sql += 'default tablespace %s quota unlimited on %s ' % (default_tablespace, default_tablespace) + if temporary_tablespace: + user_sql += 'temporary tablespace %s ' % temporary_tablespace + extend_sql = 'grant connect to %s' % username + + rtn, err = oracle_client.execute(user_sql) + if err: + module.fail_json(msg='Cannot add/edit user %s: %s' % (username, err), changed=False) + else: + if extend_sql: + oracle_client.execute(extend_sql) + module.exit_json(msg='User %s has been created.' % username, changed=True, name=username) + + +def user_remove(module, oracle_client, username): + user = user_find(oracle_client, username) + + if user: + rtn, err = oracle_client.execute('drop user %s cascade' % username) + if err: + module.fail_json(msg='Cannot drop user %s: %s' % (username, err), changed=False) + else: + module.exit_json(msg='User %s dropped.' % username, changed=True, name=username) + else: + module.exit_json(msg="User %s doesn't exist." % username, changed=False, name=username) + + +# ========================================= +# Module execution. +# + +def main(): + argument_spec = oracle_common_argument_spec() + argument_spec.update( + authentication_type=dict( + type='str', required=False, + choices=['external', 'global', 'no_authentication', 'password'] + ), + default_tablespace=dict(required=False, aliases=['db']), + name=dict(required=True, aliases=['user']), + password=dict(aliases=['pass'], no_log=True), + state=dict(type='str', default='present', choices=['absent', 'present']), + update_password=dict(default="always", choices=["always", "on_create"], no_log=False), + temporary_tablespace=dict(type='str', default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + authentication_type = module.params['authentication_type'] or 'password' + default_tablespace = module.params['default_tablespace'] + user = module.params['name'] + password = module.params['password'] + state = module.params['state'] + update_password = module.params['update_password'] + temporary_tablespace = module.params['temporary_tablespace'] + + oracle_client = OracleClient(module) + if state == 'present': + if password is None and update_password == 'always': + module.fail_json( + msg='password parameter required when adding a user unless update_password is set to on_create' + ) + user_add( + module, oracle_client, username=user, password=password, + auth_type=authentication_type, default_tablespace=default_tablespace, + temporary_tablespace=temporary_tablespace + ) + elif state == 'absent': + user_remove(oracle_client) + module.exit_json(changed=True, user=user) + + +if __name__ == '__main__': + main() diff --git a/apps/ops/ansible/modules_utils/__init__.py b/apps/ops/ansible/modules_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ops/ansible/modules_utils/oracle_common.py b/apps/ops/ansible/modules_utils/oracle_common.py new file mode 100644 index 000000000..c88f04373 --- /dev/null +++ b/apps/ops/ansible/modules_utils/oracle_common.py @@ -0,0 +1,94 @@ +import os + +import oracledb + +from oracledb.exceptions import DatabaseError +from ansible.module_utils._text import to_native + + +def oracle_common_argument_spec(): + """ + Returns a dict containing common options shared across the Oracle modules. + """ + options = dict( + login_user=dict(type='str', required=False), + login_password=dict(type='str', required=False, no_log=True), + login_database=dict(type='str', required=False, default='test'), + login_host=dict(type='str', required=False, default='localhost'), + login_port=dict(type='int', required=False, default=1521), + oracle_home=dict(type='str', required=False), + mode=dict(type='str', required=False), + ) + return options + + +class OracleClient(object): + def __init__(self, module): + self.module = module + self._conn = None + self._cursor = None + self.connect_params = {} + + self.init_params() + + def init_params(self): + params = self.module.params + hostname = params['login_host'] + port = params['login_port'] + service_name = params['login_database'] + username = params['login_user'] + password = params['login_password'] + oracle_home = params['oracle_home'] + mode = params['mode'] + + if oracle_home: + os.environ.setdefault('ORACLE_HOME', oracle_home) + if mode == 'sysdba': + self.connect_params['mode'] = oracledb.SYSDBA + + self.connect_params['host'] = hostname + self.connect_params['port'] = port + self.connect_params['user'] = username + self.connect_params['password'] = password + self.connect_params['service_name'] = service_name + + @property + def cursor(self): + if self._cursor is None: + try: + oracledb.init_oracle_client(lib_dir='/Users/jiangweidong/Downloads/instantclient_19_8') + self._conn = oracledb.connect(**self.connect_params) + self._cursor = self._conn.cursor() + except DatabaseError as err: + self.module.fail_json( + msg="Unable to connect to database: %s, %s" % (to_native(err), self.connect_params) + ) + return self._cursor + + def execute(self, sql, exception_to_fail=False): + sql = sql[:-1] if sql.endswith(';') else sql + result, error = None, None + try: + self.cursor.execute(sql) + sql_header = self.cursor.description or [] + column_names = [description[0].lower() for description in sql_header] + if column_names: + result = [dict(zip(column_names, row)) for row in self.cursor] + result = result[0] if len(result) == 1 else result + else: + result = None + except DatabaseError as err: + error = err + if exception_to_fail and error: + self.module.fail_json(msg='Cannot execute sql: %s' % to_native(error)) + return result, error + + def close(self): + try: + if self._cursor: + self._cursor.close() + if self._conn: + self._conn.close() + except: + pass + From 7ac9681f0dfb8ccfd108d1921772ca54447d90f9 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 16 Nov 2022 14:27:50 +0800 Subject: [PATCH 02/12] perf: asyncio ws task log --- apps/ops/ws.py | 107 +++++++++++++++------------------- requirements/requirements.txt | 1 + 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/apps/ops/ws.py b/apps/ops/ws.py index 94d71d90d..473093ba2 100644 --- a/apps/ops/ws.py +++ b/apps/ops/ws.py @@ -1,18 +1,18 @@ -import time +import asyncio import os -import threading -import json -from channels.generic.websocket import JsonWebsocketConsumer -from common.utils import get_logger +import aiofiles +from channels.generic.websocket import AsyncJsonWebsocketConsumer + from common.db.utils import close_old_connections -from .celery.utils import get_celery_task_log_path +from common.utils import get_logger from .ansible.utils import get_ansible_task_log_path +from .celery.utils import get_celery_task_log_path logger = get_logger(__name__) -class TaskLogWebsocket(JsonWebsocketConsumer): +class TaskLogWebsocket(AsyncJsonWebsocketConsumer): disconnected = False log_types = { @@ -20,70 +20,59 @@ class TaskLogWebsocket(JsonWebsocketConsumer): 'ansible': get_ansible_task_log_path } - def connect(self): + async def connect(self): user = self.scope["user"] if user.is_authenticated: - self.accept() + await self.accept() else: - self.close() + await self.close() - def get_log_path(self, task_id): - func = self.log_types.get(self.log_type) + def get_log_path(self, task_id, log_type): + func = self.log_types.get(log_type) if func: return func(task_id) - def receive(self, text_data=None, bytes_data=None, **kwargs): - data = json.loads(text_data) - task_id = data.get('task') - self.log_type = data.get('type', 'celery') - if task_id: - self.handle_task(task_id) + async def receive_json(self, content, **kwargs): + task_id = content.get('task') + task_typ = content.get('type', 'celery') + log_path = self.get_log_path(task_id, task_typ) + await self.async_handle_task(task_id, log_path) - def wait_util_log_path_exist(self, task_id): - log_path = self.get_log_path(task_id) + async def async_handle_task(self, task_id, log_path): + logger.info("Task id: {}".format(task_id)) while not self.disconnected: if not os.path.exists(log_path): - self.send_json({'message': '.', 'task': task_id}) - time.sleep(0.5) - continue - self.send_json({'message': '\r\n'}) - try: - logger.debug('Task log path: {}'.format(log_path)) - task_log_f = open(log_path, 'rb') - return task_log_f - except OSError: - return None - - def read_log_file(self, task_id): - task_log_f = self.wait_util_log_path_exist(task_id) - if not task_log_f: - logger.debug('Task log file is None: {}'.format(task_id)) - return - - task_end_mark = [] - while not self.disconnected: - data = task_log_f.read(4096) - if data: - data = data.replace(b'\n', b'\r\n') - self.send_json( - {'message': data.decode(errors='ignore'), 'task': task_id} - ) - if data.find(b'succeeded in') != -1: - task_end_mark.append(1) - if data.find(bytes(task_id, 'utf8')) != -1: - task_end_mark.append(1) - elif len(task_end_mark) == 2: - logger.debug('Task log end: {}'.format(task_id)) + await self.send_json({'message': '.', 'task': task_id}) + await asyncio.sleep(0.5) + else: + await self.send_task_log(task_id, log_path) break - time.sleep(0.2) - task_log_f.close() - def handle_task(self, task_id): - logger.info("Task id: {}".format(task_id)) - thread = threading.Thread(target=self.read_log_file, args=(task_id,)) - thread.start() + async def send_task_log(self, task_id, log_path): + await self.send_json({'message': '\r\n'}) + try: + logger.debug('Task log path: {}'.format(log_path)) + task_end_mark = [] + async with aiofiles.open(log_path, 'rb') as task_log_f: + while not self.disconnected: + data = await task_log_f.read(4096) + if data: + data = data.replace(b'\n', b'\r\n') + await self.send_json( + {'message': data.decode(errors='ignore'), 'task': task_id} + ) + if data.find(b'succeeded in') != -1: + task_end_mark.append(1) + if data.find(bytes(task_id, 'utf8')) != -1: + task_end_mark.append(1) + elif len(task_end_mark) == 2: + logger.debug('Task log end: {}'.format(task_id)) + break + await asyncio.sleep(0.2) + except OSError as e: + logger.warn('Task log path open failed: {}'.format(e)) - def disconnect(self, close_code): + async def disconnect(self, close_code): self.disconnected = True - self.close() + await self.close() close_old_connections() diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8af2fcb0f..659add611 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,3 +1,4 @@ +aiofiles==22.1.0 amqp==5.0.9 ansible==6.4.0 ansible-runner==2.2.1 From 9e41ad076418ee012285c4bf0873bf8795ef13a4 Mon Sep 17 00:00:00 2001 From: jiangweidong Date: Thu, 24 Nov 2022 15:25:09 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=9B=B4=E6=96=B0=E7=95=8C=E9=9D=A2=E6=B8=85=E7=A9=BA?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9A=84=E7=A7=98=E9=92=A5=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/base.py | 2 +- apps/assets/serializers/account/account.py | 4 +++- apps/assets/serializers/base.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 90fb384e6..3609ca270 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -83,7 +83,7 @@ class BaseAccount(JMSOrgBaseModel): @lazyproperty def public_key(self): - if self.secret_type == SecretType.SSH_KEY: + if self.secret_type == SecretType.SSH_KEY and self.private_key: return ssh_pubkey_gen(private_key=self.private_key) return None diff --git a/apps/assets/serializers/account/account.py b/apps/assets/serializers/account/account.py index cfd9a52f4..c7dad4f5b 100644 --- a/apps/assets/serializers/account/account.py +++ b/apps/assets/serializers/account/account.py @@ -72,7 +72,9 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer): def __init__(self, *args, data=None, **kwargs): super().__init__(*args, data=data, **kwargs) if data and 'name' not in data: - data['name'] = data.get('username') + username = data.get('username') + if username is not None: + data['name'] = username if hasattr(self, 'initial_data') and \ not getattr(self, 'initial_data', None): delattr(self, 'initial_data') diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 18432a7e5..9641ce786 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -28,7 +28,7 @@ class AuthValidateMixin(serializers.Serializer): def validate_secret(self, secret): if not secret: - return + return '' secret_type = self.initial_secret_type if secret_type == SecretType.PASSWORD: validate_password_for_ansible(secret) @@ -44,7 +44,7 @@ class AuthValidateMixin(serializers.Serializer): def clean_auth_fields(validated_data): for field in ('secret',): value = validated_data.get(field) - if not value: + if value is None: validated_data.pop(field, None) validated_data.pop('passphrase', None) From 7a475fc029609f84049e514c75274631a20b80a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Thu, 1 Dec 2022 14:54:57 +0800 Subject: [PATCH 04/12] =?UTF-8?q?perf:=20=E6=8E=A7=E5=88=B6=20gunicorn=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/commands/services/services/gunicorn.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/common/management/commands/services/services/gunicorn.py b/apps/common/management/commands/services/services/gunicorn.py index 5cc67b45c..97db306a7 100644 --- a/apps/common/management/commands/services/services/gunicorn.py +++ b/apps/common/management/commands/services/services/gunicorn.py @@ -1,3 +1,4 @@ +import multiprocessing from ..hands import * from .base import BaseService @@ -16,11 +17,15 @@ class GunicornService(BaseService): log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' bind = f'{HTTP_HOST}:{HTTP_PORT}' + cores = 10 + if (multiprocessing.cpu_count() * 2 + 1) < cores: + cores = multiprocessing.cpu_count() * 2 + 1 + cmd = [ 'gunicorn', 'jumpserver.asgi:application', '-b', bind, '-k', 'uvicorn.workers.UvicornWorker', - '--threads', '10', + '--threads', str(cores), '-w', str(self.worker), '--max-requests', '4096', '--access-logformat', log_format, From 2bc47c87d1077ff70721f9a5d4edbf9202c35341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Thu, 1 Dec 2022 16:12:10 +0800 Subject: [PATCH 05/12] =?UTF-8?q?perf:=20=E4=BF=AE=E6=AD=A3=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/management/commands/services/command.py | 7 ++++++- .../management/commands/services/services/gunicorn.py | 6 +----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/common/management/commands/services/command.py b/apps/common/management/commands/services/command.py index 1fb28fd3a..2ee11385d 100644 --- a/apps/common/management/commands/services/command.py +++ b/apps/common/management/commands/services/command.py @@ -1,3 +1,4 @@ +import multiprocessing from django.core.management.base import BaseCommand, CommandError from django.db.models import TextChoices from .utils import ServicesUtil @@ -91,11 +92,15 @@ class BaseActionCommand(BaseCommand): super().__init__(*args, **kwargs) def add_arguments(self, parser): + cores = 10 + if (multiprocessing.cpu_count() + 1) < cores: + cores = multiprocessing.cpu_count() + 1 + parser.add_argument( 'services', nargs='+', choices=Services.export_services_values(), help='Service', ) parser.add_argument('-d', '--daemon', nargs="?", const=True) - parser.add_argument('-w', '--worker', type=int, nargs="?", default=4) + parser.add_argument('-w', '--worker', type=int, nargs="?", default=cores) parser.add_argument('-f', '--force', nargs="?", const=True) def initial_util(self, *args, **options): diff --git a/apps/common/management/commands/services/services/gunicorn.py b/apps/common/management/commands/services/services/gunicorn.py index 97db306a7..495ace6c7 100644 --- a/apps/common/management/commands/services/services/gunicorn.py +++ b/apps/common/management/commands/services/services/gunicorn.py @@ -1,4 +1,3 @@ -import multiprocessing from ..hands import * from .base import BaseService @@ -17,15 +16,12 @@ class GunicornService(BaseService): log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' bind = f'{HTTP_HOST}:{HTTP_PORT}' - cores = 10 - if (multiprocessing.cpu_count() * 2 + 1) < cores: - cores = multiprocessing.cpu_count() * 2 + 1 cmd = [ 'gunicorn', 'jumpserver.asgi:application', '-b', bind, '-k', 'uvicorn.workers.UvicornWorker', - '--threads', str(cores), + '--threads', str(self.worker * 2), '-w', str(self.worker), '--max-requests', '4096', '--access-logformat', log_format, From ce3ec851477d6de6d054b27a4b9c422afa40b46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Thu, 1 Dec 2022 16:22:26 +0800 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20=E5=8E=BB=E6=8E=89=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jms b/jms index 5d5b5c433..57410c933 100755 --- a/jms +++ b/jms @@ -177,7 +177,7 @@ if __name__ == '__main__': help="The service to start", ) parser.add_argument('-d', '--daemon', nargs="?", const=True) - parser.add_argument('-w', '--worker', type=int, nargs="?", default=4) + parser.add_argument('-w', '--worker', type=int, nargs="?") parser.add_argument('-f', '--force', nargs="?", const=True) args = parser.parse_args() From 709b6e5b0dc25745a972f8843a729b882426cfc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Fri, 2 Dec 2022 10:45:12 +0800 Subject: [PATCH 07/12] =?UTF-8?q?perf:=20=E5=8E=BB=E6=8E=89=20gunicorn=20t?= =?UTF-8?q?hreads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/management/commands/services/command.py | 4 ++-- apps/common/management/commands/services/services/gunicorn.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/common/management/commands/services/command.py b/apps/common/management/commands/services/command.py index 2ee11385d..fcaa8f1cd 100644 --- a/apps/common/management/commands/services/command.py +++ b/apps/common/management/commands/services/command.py @@ -93,8 +93,8 @@ class BaseActionCommand(BaseCommand): def add_arguments(self, parser): cores = 10 - if (multiprocessing.cpu_count() + 1) < cores: - cores = multiprocessing.cpu_count() + 1 + if (multiprocessing.cpu_count() * 2 + 1) < cores: + cores = multiprocessing.cpu_count() * 2 + 1 parser.add_argument( 'services', nargs='+', choices=Services.export_services_values(), help='Service', diff --git a/apps/common/management/commands/services/services/gunicorn.py b/apps/common/management/commands/services/services/gunicorn.py index 495ace6c7..5eab30ec3 100644 --- a/apps/common/management/commands/services/services/gunicorn.py +++ b/apps/common/management/commands/services/services/gunicorn.py @@ -21,7 +21,6 @@ class GunicornService(BaseService): 'gunicorn', 'jumpserver.asgi:application', '-b', bind, '-k', 'uvicorn.workers.UvicornWorker', - '--threads', str(self.worker * 2), '-w', str(self.worker), '--max-requests', '4096', '--access-logformat', log_format, From a18f544cf86f74e92797eaa955b6610a4409d7a3 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 2 Dec 2022 11:12:14 +0800 Subject: [PATCH 08/12] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20=20acl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0005_auto_20221201_1846.py | 5 +- .../0006_commandfilteracl_commandgroup.py | 27 ++-- .../migrations/0007_auto_20221202_1048.py | 21 +++ apps/acls/models/base.py | 45 ++++-- apps/acls/models/command_acl.py | 33 +++-- apps/acls/models/login_asset_acl.py | 34 +---- apps/acls/serializers/base.py | 94 +++++++++++++ apps/acls/serializers/command_filter.py | 16 +++ apps/acls/serializers/login_asset_acl.py | 108 +-------------- apps/assets/models/cmd_filter.py | 130 +----------------- apps/authentication/mixins.py | 12 +- 11 files changed, 217 insertions(+), 308 deletions(-) create mode 100644 apps/acls/migrations/0007_auto_20221202_1048.py create mode 100644 apps/acls/serializers/base.py diff --git a/apps/acls/migrations/0005_auto_20221201_1846.py b/apps/acls/migrations/0005_auto_20221201_1846.py index b69216896..4885ea97e 100644 --- a/apps/acls/migrations/0005_auto_20221201_1846.py +++ b/apps/acls/migrations/0005_auto_20221201_1846.py @@ -5,7 +5,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('acls', '0004_auto_20220831_1658'), @@ -15,7 +14,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='loginacl', name='action', - field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'), + field=models.CharField(default='reject', max_length=64, verbose_name='Action'), ), migrations.AlterField( model_name='loginacl', @@ -25,7 +24,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='loginassetacl', name='action', - field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'), + field=models.CharField(default='reject', max_length=64, verbose_name='Action'), ), migrations.AlterField( model_name='loginassetacl', diff --git a/apps/acls/migrations/0006_commandfilteracl_commandgroup.py b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py index 05122b733..95b12e9f0 100644 --- a/apps/acls/migrations/0006_commandfilteracl_commandgroup.py +++ b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py @@ -1,13 +1,13 @@ # Generated by Django 3.2.14 on 2022-12-01 11:39 -from django.conf import settings -import django.core.validators -from django.db import migrations, models import uuid +import django.core.validators +from django.conf import settings +from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('acls', '0005_auto_20221201_1846'), @@ -22,9 +22,11 @@ class Migration(migrations.Migration): ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command', max_length=16, verbose_name='Type')), + ('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command', + max_length=16, verbose_name='Type')), ('content', models.TextField(help_text='One line one command', verbose_name='Content')), ('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')), ], @@ -36,21 +38,26 @@ class Migration(migrations.Migration): migrations.CreateModel( name='CommandFilterACL', fields=[ - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), - ('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action')), + ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', + validators=[django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(100)], + verbose_name='Priority')), + ('action', models.CharField(default='reject', max_length=64, verbose_name='Action')), ('is_active', models.BooleanField(default=True, verbose_name='Active')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('users', models.JSONField(verbose_name='User')), ('accounts', models.JSONField(verbose_name='Account')), ('assets', models.JSONField(verbose_name='Asset')), ('commands', models.ManyToManyField(to='acls.CommandGroup', verbose_name='Commands')), - ('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), + ( + 'reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), ], options={ 'verbose_name': 'Command acl', diff --git a/apps/acls/migrations/0007_auto_20221202_1048.py b/apps/acls/migrations/0007_auto_20221202_1048.py new file mode 100644 index 000000000..1a61a4ff4 --- /dev/null +++ b/apps/acls/migrations/0007_auto_20221202_1048.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.14 on 2022-12-02 02:48 + +from django.db import migrations + + +def migrate_login_type(apps, schema_editor): + login_asset_model = apps.get_model('acls', 'LoginAssetACL') + login_asset_model.objects.filter(action='login_confirm').update(action='review') + + login_system_model = apps.get_model('acls', 'LoginACL') + login_system_model.objects.filter(action='confirm').update(action='review') + + +class Migration(migrations.Migration): + dependencies = [ + ('acls', '0006_commandfilteracl_commandgroup'), + ] + + operations = [ + migrations.RunPython(migrate_login_type), + ] diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 6bda02df8..33e1fbc2a 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -1,16 +1,18 @@ -from django.db import models -from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +from django.db import models +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ + from common.mixins import CommonModelMixin +from common.utils import contains_ip - -__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager'] +__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager', 'AssetAccountUserACLQuerySet'] class ActionChoices(models.TextChoices): reject = 'reject', _('Reject') - allow = 'allow', _('Allow') - confirm = 'confirm', _('Confirm') + accept = 'allow', _('Allow') + review = 'review', _('Review') class BaseACLQuerySet(models.QuerySet): @@ -27,6 +29,32 @@ class BaseACLQuerySet(models.QuerySet): return self.inactive() +class AssetAccountUserACLQuerySet(BaseACLQuerySet): + def filter_user(self, user): + return self.filter( + Q(users__username_group__contains=user.username) | + Q(users__username_group__contains='*') + ) + + def filter_asset(self, asset): + queryset = self.filter( + Q(assets__name_group__contains=asset.name) | + Q(assets__name_group__contains='*') + ) + ids = [ + q.id for q in queryset + if contains_ip(asset.address, q.assets.get('address_group', [])) + ] + queryset = self.filter(id__in=ids) + return queryset + + def filter_account(self, account_username): + return self.filter( + Q(accounts__username_group__contains=account_username) | + Q(accounts__username_group__contains='*') + ) + + class ACLManager(models.Manager): def valid(self): return self.get_queryset().valid() @@ -39,10 +67,7 @@ class BaseACL(CommonModelMixin): help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)] ) - action = models.CharField( - max_length=64, verbose_name=_('Action'), - choices=ActionChoices.choices, default=ActionChoices.reject - ) + action = models.CharField(max_length=64, default=ActionChoices.reject, verbose_name=_('Action')) reviewers = models.ManyToManyField('users.User', blank=True, verbose_name=_("Reviewers")) is_active = models.BooleanField(default=True, verbose_name=_("Active")) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index 4fba67a3d..056d9b4f6 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -6,11 +6,11 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from users.models import User, UserGroup -from orgs.mixins.models import JMSOrgBaseModel from common.utils import lazyproperty, get_logger, get_object_or_none +from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import OrgModelMixin -from .base import BaseACL +from users.models import User, UserGroup +from .base import BaseACL, AssetAccountUserACLQuerySet, ACLManager logger = get_logger(__file__) @@ -50,7 +50,6 @@ class CommandGroup(JMSOrgBaseModel): if ' ' in _cmd: regex.append(cmd) continue - if not cmd: continue @@ -89,6 +88,19 @@ class CommandGroup(JMSOrgBaseModel): def __str__(self): return '{} % {}'.format(self.type, self.content) + +class CommandFilterACL(OrgModelMixin, BaseACL): + users = models.JSONField(verbose_name=_('User')) + assets = models.JSONField(verbose_name=_('Asset')) + accounts = models.JSONField(verbose_name=_('Account')) + commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands')) + objects = ACLManager.from_queryset(AssetAccountUserACLQuerySet)() + + class Meta: + unique_together = ('name', 'org_id') + ordering = ('priority', '-date_updated', 'name') + verbose_name = _('Command acl') + def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): from tickets.const import TicketType from tickets.models import ApplyCommandTicket @@ -147,16 +159,3 @@ class CommandGroup(JMSOrgBaseModel): else: rules = cls.objects.none() return rules - - -class CommandFilterACL(OrgModelMixin, BaseACL): - # 条件 - users = models.JSONField(verbose_name=_('User')) - accounts = models.JSONField(verbose_name=_('Account')) - assets = models.JSONField(verbose_name=_('Asset')) - commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands')) - - class Meta: - unique_together = ('name', 'org_id') - ordering = ('priority', '-date_updated', 'name') - verbose_name = _('Command acl') diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index de9897c7b..6af48faab 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -1,35 +1,8 @@ from django.db import models -from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.models import OrgModelMixin, OrgManager -from .base import BaseACL, BaseACLQuerySet, ACLManager -from common.utils.ip import contains_ip - -class ACLQuerySet(BaseACLQuerySet): - def filter_user(self, user): - return self.filter( - Q(users__username_group__contains=user.username) | - Q(users__username_group__contains='*') - ) - - def filter_asset(self, asset): - queryset = self.filter( - Q(assets__name_group__contains=asset.name) | - Q(assets__name_group__contains='*') - ) - ids = [ - q.id for q in queryset - if contains_ip(asset.address, q.assets.get('address_group', [])) - ] - queryset = LoginAssetACL.objects.filter(id__in=ids) - return queryset - - def filter_account(self, account_username): - return self.filter( - Q(accounts__username_group__contains=account_username) | - Q(accounts__username_group__contains='*') - ) +from orgs.mixins.models import OrgModelMixin +from .base import BaseACL, ACLManager, AssetAccountUserACLQuerySet class LoginAssetACL(BaseACL, OrgModelMixin): @@ -38,7 +11,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin): accounts = models.JSONField(verbose_name=_('Account')) assets = models.JSONField(verbose_name=_('Asset')) - objects = ACLManager.from_queryset(ACLQuerySet)() + objects = ACLManager.from_queryset(AssetAccountUserACLQuerySet)() class Meta: unique_together = ('name', 'org_id') @@ -65,4 +38,3 @@ class LoginAssetACL(BaseACL, OrgModelMixin): ticket = ApplyLoginAssetTicket.objects.create(**data) ticket.open_by_system(assignees) return ticket - diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py new file mode 100644 index 000000000..9f511cd06 --- /dev/null +++ b/apps/acls/serializers/base.py @@ -0,0 +1,94 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from acls.models.base import ActionChoices +from common.drf.fields import LabeledChoiceField, ObjectRelatedField +from orgs.models import Organization +from users.models import User + +common_help_text = _( + "Format for comma-delimited string, with * indicating a match all. " +) + + +class ACLUsersSerializer(serializers.Serializer): + username_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, + ) + + +class ACLAssestsSerializer(serializers.Serializer): + address_group_help_text = _( + "Format for comma-delimited string, with * indicating a match all. " + "Such as: " + "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" + " (Domain name support)" + ) + + name_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Name"), + help_text=common_help_text, + ) + address_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=1024), + label=_("IP/Host"), + help_text=address_group_help_text, + ) + + +class ACLAccountsSerializer(serializers.Serializer): + username_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=128), + label=_("Username"), + help_text=common_help_text, + ) + + +class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): + users = ACLUsersSerializer() + assets = ACLAssestsSerializer() + accounts = ACLAccountsSerializer() + reviewers = ObjectRelatedField( + queryset=User.objects, many=True, required=False, label=_('Reviewers') + ) + reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") + action = LabeledChoiceField( + choices=ActionChoices.choices, label=_("Action") + ) + + class Meta: + fields_mini = ["id", "name"] + fields_small = fields_mini + [ + "users", "accounts", "assets", "is_active", + "date_created", "date_updated", "priority", + "action", "comment", "created_by", "org_id", + ] + fields_m2m = ["reviewers", "reviewers_amount"] + fields = fields_small + fields_m2m + extra_kwargs = { + "reviewers": {"allow_null": False, "required": True}, + "priority": {"default": 50}, + "is_active": {"default": True}, + } + + def validate_reviewers(self, reviewers): + org_id = self.fields["org_id"].default() + org = Organization.get_instance(org_id) + if not org: + error = _("The organization `{}` does not exist".format(org_id)) + raise serializers.ValidationError(error) + users = org.get_members() + valid_reviewers = list(set(reviewers) & set(users)) + if not valid_reviewers: + error = _( + "None of the reviewers belong to Organization `{}`".format(org.name) + ) + raise serializers.ValidationError(error) + return valid_reviewers diff --git a/apps/acls/serializers/command_filter.py b/apps/acls/serializers/command_filter.py index e69de29bb..a6b090c42 100644 --- a/apps/acls/serializers/command_filter.py +++ b/apps/acls/serializers/command_filter.py @@ -0,0 +1,16 @@ +from django.utils.translation import ugettext_lazy as _ + +from acls.models import CommandGroup, CommandFilterACL +from common.drf.fields import ObjectRelatedField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .base import BaseUserAssetAccountACLSerializerMixin + +__all__ = ["CommandFilterACLSerializer"] + + +class CommandFilterACLSerializer(BaseUserAssetAccountACLSerializerMixin, BulkOrgResourceModelSerializer): + commands = ObjectRelatedField(queryset=CommandGroup.objects, many=True, required=False, label=_('Commands')) + + class Meta(BaseUserAssetAccountACLSerializerMixin.Meta): + model = CommandFilterACL + fields = BaseUserAssetAccountACLSerializerMixin.Meta.fields + ['commands'] diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 6e3e6bc50..b360bb55f 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -1,109 +1,11 @@ -from rest_framework import serializers -from django.utils.translation import ugettext_lazy as _ - -from common.drf.fields import LabeledChoiceField -from common.drf.fields import ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from orgs.models import Organization -from users.models import User -from acls import models +from .base import BaseUserAssetAccountACLSerializerMixin +from ..models import LoginAssetACL __all__ = ["LoginAssetACLSerializer"] -common_help_text = _( - "Format for comma-delimited string, with * indicating a match all. " -) - - -class LoginAssetACLUsersSerializer(serializers.Serializer): - username_group = serializers.ListField( - default=["*"], - child=serializers.CharField(max_length=128), - label=_("Username"), - help_text=common_help_text, - ) - - -class LoginAssetACLAssestsSerializer(serializers.Serializer): - address_group_help_text = _( - "Format for comma-delimited string, with * indicating a match all. " - "Such as: " - "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" - " (Domain name support)" - ) - - name_group = serializers.ListField( - default=["*"], - child=serializers.CharField(max_length=128), - label=_("Name"), - help_text=common_help_text, - ) - address_group = serializers.ListField( - default=["*"], - child=serializers.CharField(max_length=1024), - label=_("IP/Host"), - help_text=address_group_help_text, - ) - - -class LoginAssetACLAccountsSerializer(serializers.Serializer): - username_group = serializers.ListField( - default=["*"], - child=serializers.CharField(max_length=128), - label=_("Username"), - help_text=common_help_text, - ) - - -class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): - users = LoginAssetACLUsersSerializer() - assets = LoginAssetACLAssestsSerializer() - accounts = LoginAssetACLAccountsSerializer() - reviewers = ObjectRelatedField( - queryset=User.objects, many=True, required=False, label=_('Reviewers') - ) - reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") - action = LabeledChoiceField( - choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action") - ) - - class Meta: - model = models.LoginAssetACL - fields_mini = ["id", "name"] - fields_small = fields_mini + [ - "users", - "accounts", - "assets", - "is_active", - "date_created", - "date_updated", - "priority", - "action", - "comment", - "created_by", - "org_id", - ] - fields_m2m = ["reviewers", "reviewers_amount"] - fields = fields_small + fields_m2m - extra_kwargs = { - "reviewers": {"allow_null": False, "required": True}, - "priority": {"default": 50}, - "is_active": {"default": True}, - } - - def validate_reviewers(self, reviewers): - org_id = self.fields["org_id"].default() - org = Organization.get_instance(org_id) - if not org: - error = _("The organization `{}` does not exist".format(org_id)) - raise serializers.ValidationError(error) - users = org.get_members() - valid_reviewers = list(set(reviewers) & set(users)) - if not valid_reviewers: - error = _( - "None of the reviewers belong to Organization `{}`".format(org.name) - ) - raise serializers.ValidationError(error) - return valid_reviewers +class LoginAssetACLSerializer(BaseUserAssetAccountACLSerializerMixin, BulkOrgResourceModelSerializer): + class Meta(BaseUserAssetAccountACLSerializerMixin.Meta): + model = LoginAssetACL diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 5b9b1ad85..5f4adebcf 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -1,17 +1,13 @@ # -*- coding: utf-8 -*- # -import re import uuid -from django.db import models -from django.db.models import Q from django.core.validators import MinValueValidator, MaxValueValidator +from django.db import models from django.utils.translation import ugettext_lazy as _ -from users.models import User, UserGroup +from common.utils import get_logger from orgs.mixins.models import OrgModelMixin -from common.utils import lazyproperty, get_logger, get_object_or_none -from ..models import Asset, Account logger = get_logger(__file__) @@ -93,125 +89,3 @@ class CommandFilterRule(OrgModelMixin): class Meta: ordering = ('priority', 'action') verbose_name = _("Command filter rule") - - @lazyproperty - def pattern(self): - if self.type == 'command': - s = self.construct_command_regex(content=self.content) - else: - s = r'{0}'.format(self.content) - return s - - @classmethod - def construct_command_regex(cls, content): - regex = [] - content = content.replace('\r\n', '\n') - for _cmd in content.split('\n'): - cmd = re.sub(r'\s+', ' ', _cmd) - cmd = re.escape(cmd) - cmd = cmd.replace('\\ ', '\s+') - - # 有空格就不能 铆钉单词了 - if ' ' in _cmd: - regex.append(cmd) - continue - - if not cmd: - continue - - # 如果是单个字符 - if cmd[-1].isalpha(): - regex.append(r'\b{0}\b'.format(cmd)) - else: - regex.append(r'\b{0}'.format(cmd)) - s = r'{}'.format('|'.join(regex)) - return s - - @staticmethod - def compile_regex(regex, ignore_case): - try: - if ignore_case: - pattern = re.compile(regex, re.IGNORECASE) - else: - pattern = re.compile(regex) - except Exception as e: - error = _('The generated regular expression is incorrect: {}').format(str(e)) - logger.error(error) - return False, error, None - return True, '', pattern - - def match(self, data): - succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case) - if not succeed: - return self.ACTION_UNKNOWN, '' - - found = pattern.search(data) - if not found: - return self.ACTION_UNKNOWN, '' - - if self.action == self.ActionChoices.allow: - return self.ActionChoices.allow, found.group() - else: - return self.ActionChoices.deny, found.group() - - def __str__(self): - return '{} % {}'.format(self.type, self.content) - - def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): - from tickets.const import TicketType - from tickets.models import ApplyCommandTicket - data = { - 'title': _('Command confirm') + ' ({})'.format(session.user), - 'type': TicketType.command_confirm, - 'applicant': session.user_obj, - 'apply_run_user_id': session.user_id, - 'apply_run_asset': str(session.asset), - 'apply_run_account': str(session.account), - 'apply_run_command': run_command[:4090], - 'apply_from_session_id': str(session.id), - 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), - 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id), - 'org_id': org_id, - } - ticket = ApplyCommandTicket.objects.create(**data) - assignees = self.reviewers.all() - ticket.open_by_system(assignees) - return ticket - - @classmethod - def get_queryset( - cls, user_id=None, user_group_id=None, account=None, - asset_id=None, org_id=None - ): - from assets.models import Account - user_groups = [] - user = get_object_or_none(User, pk=user_id) - if user: - user_groups.extend(list(user.groups.all())) - user_group = get_object_or_none(UserGroup, pk=user_group_id) - if user_group: - org_id = user_group.org_id - user_groups.append(user_group) - - asset = get_object_or_none(Asset, pk=asset_id) - q = Q() - if user: - q |= Q(users=user) - if user_groups: - q |= Q(user_groups__in=set(user_groups)) - if account: - org_id = account.org_id - q |= Q(accounts__contains=account.username) | \ - Q(accounts__contains=Account.AliasAccount.ALL) - if asset: - org_id = asset.org_id - q |= Q(assets=asset) - if q: - cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) - if org_id: - cmd_filters = cmd_filters.filter(org_id=org_id) - rule_ids = cmd_filters.values_list('rules', flat=True) - rules = cls.objects.filter(id__in=rule_ids) - else: - rules = cls.objects.none() - return rules diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 7341b4bd1..ec6d2e98d 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- # import inspect -from functools import partial import time +from functools import partial from typing import Callable -from django.utils.http import urlencode -from django.core.cache import cache from django.conf import settings from django.contrib import auth -from django.utils.translation import ugettext as _ -from rest_framework.request import Request from django.contrib.auth import ( BACKEND_SESSION_KEY, load_backend, PermissionDenied, user_login_failed, _clean_credentials, ) +from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from django.shortcuts import reverse, redirect, get_object_or_404 +from django.utils.http import urlencode +from django.utils.translation import ugettext as _ +from rest_framework.request import Request -from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil from acls.models import LoginACL +from common.utils import get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil from . import errors From 19c3f98e8fbc4d333180551051c25d171f628d2b Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 2 Dec 2022 11:14:29 +0800 Subject: [PATCH 09/12] =?UTF-8?q?pref:=20=E4=BF=AE=E6=94=B9=20migrations?= =?UTF-8?q?=20=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/audits/migrations/0016_alter_userloginlog_type.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/audits/migrations/0016_alter_userloginlog_type.py b/apps/audits/migrations/0016_alter_userloginlog_type.py index 86145721d..1047a4a30 100644 --- a/apps/audits/migrations/0016_alter_userloginlog_type.py +++ b/apps/audits/migrations/0016_alter_userloginlog_type.py @@ -4,15 +4,17 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('audits', '0015_auto_20221011_1745'), + ('audits', '0015_auto_20221111_1919'), ] operations = [ migrations.AlterField( model_name='userloginlog', name='type', - field=models.CharField(choices=[('web_cli', 'Web Client'), ('web_gui', 'Web GUI'), ('db_cli', 'DB Client'), ('db_gui', 'DB GUI'), ('rdp_cli', 'RDP Client'), ('rdp_file', 'RDP File'), ('ssh_cli', 'SSH Client'), ('web_sftp', 'Web SFTP')], max_length=128, verbose_name='Login type'), + field=models.CharField(choices=[('web_cli', 'Web Client'), ('web_gui', 'Web GUI'), ('db_cli', 'DB Client'), + ('db_gui', 'DB GUI'), ('rdp_cli', 'RDP Client'), ('rdp_file', 'RDP File'), + ('ssh_cli', 'SSH Client'), ('web_sftp', 'Web SFTP')], max_length=128, + verbose_name='Login type'), ), ] From 541358978d1e66449b1577996fae6dc468181fdd Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Fri, 2 Dec 2022 11:45:05 +0800 Subject: [PATCH 10/12] fix: gateway (#9145) Co-authored-by: feng <1304903146@qq.com> --- apps/assets/api/domain.py | 5 +- apps/assets/serializers/domain.py | 108 ++---------------------------- 2 files changed, 9 insertions(+), 104 deletions(-) diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 948bb6a7b..954f4842c 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -1,5 +1,4 @@ # ~*~ coding: utf-8 ~*~ -from django.db.models import F from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext as _ from rest_framework.views import APIView, Response @@ -7,7 +6,7 @@ from rest_framework.serializers import ValidationError from common.utils import get_logger from orgs.mixins.api import OrgBulkModelViewSet -from ..models import Domain, Host +from ..models import Domain, Gateway from .. import serializers logger = get_logger(__file__) @@ -29,7 +28,7 @@ class DomainViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet): - perm_model = Host + perm_model = Gateway filterset_fields = ("domain__name", "name", "domain") search_fields = ("domain__name",) serializer_class = serializers.GatewaySerializer diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index f17aa19ee..8ee651e88 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers -from rest_framework.generics import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.drf.serializers import SecretReadableMixin, WritableNestedModelSerializer -from common.drf.fields import ObjectRelatedField, EncryptedField -from assets.const import SecretType, GATEWAY_NAME -from ..serializers import AssetProtocolsSerializer -from ..models import Platform, Domain, Node, Asset, Account, Host -from .utils import validate_password_for_ansible, validate_ssh_key +from common.drf.serializers import SecretReadableMixin +from common.drf.fields import ObjectRelatedField +from ..serializers import HostSerializer +from ..models import Domain, Gateway, Asset class DomainSerializer(BulkOrgResourceModelSerializer): @@ -41,100 +38,9 @@ class DomainSerializer(BulkOrgResourceModelSerializer): return obj.gateways.count() -class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer): - password = EncryptedField( - label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024, - validators=[validate_password_for_ansible], write_only=True - ) - private_key = EncryptedField( - label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, - max_length=16384, write_only=True - ) - passphrase = serializers.CharField( - label=_('Key password'), allow_blank=True, allow_null=True, required=False, write_only=True, - max_length=512, - ) - username = serializers.CharField( - label=_('Username'), allow_blank=True, max_length=128, required=True, write_only=True - ) - username_display = serializers.SerializerMethodField(label=_('Username')) - protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) - - class Meta: - model = Host - fields_mini = ['id', 'name', 'address'] - fields_small = fields_mini + ['is_active', 'comment'] - fields = fields_small + ['domain', 'protocols'] + [ - 'username', 'password', 'private_key', 'passphrase', 'username_display' - ] - extra_kwargs = { - 'name': {'label': _("Name")}, - 'address': {'label': _('Address')}, - } - - @staticmethod - def get_username_display(obj): - account = obj.accounts.order_by('-privileged').first() - return account.username if account else '' - - def validate_private_key(self, secret): - if not secret: - return - passphrase = self.initial_data.get('passphrase') - passphrase = passphrase if passphrase else None - validate_ssh_key(secret, passphrase) - return secret - - @staticmethod - def clean_auth_fields(validated_data): - username = validated_data.pop('username', None) - password = validated_data.pop('password', None) - private_key = validated_data.pop('private_key', None) - validated_data.pop('passphrase', None) - return username, password, private_key - - @staticmethod - def create_accounts(instance, username, password, private_key): - account_name = f'{instance.name}-{_("Gateway")}' - account_data = { - 'privileged': True, - 'name': account_name, - 'username': username, - 'asset_id': instance.id, - 'created_by': instance.created_by - } - if password: - Account.objects.create( - **account_data, secret=password, secret_type=SecretType.PASSWORD - ) - if private_key: - Account.objects.create( - **account_data, secret=private_key, secret_type=SecretType.SSH_KEY - ) - - @staticmethod - def update_accounts(instance, username, password, private_key): - accounts = instance.accounts.filter(username=username) - if password: - account = get_object_or_404(accounts, SecretType.PASSWORD) - account.secret = password - account.save() - if private_key: - account = get_object_or_404(accounts, SecretType.SSH_KEY) - account.secret = private_key - account.save() - - def create(self, validated_data): - auth_fields = self.clean_auth_fields(validated_data) - instance = super().create(validated_data) - self.create_accounts(instance, *auth_fields) - return instance - - def update(self, instance, validated_data): - auth_fields = self.clean_auth_fields(validated_data) - instance = super().update(instance, validated_data) - self.update_accounts(instance, *auth_fields) - return instance +class GatewaySerializer(HostSerializer): + class Meta(HostSerializer.Meta): + model = Gateway class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer): From a6aafaec05189a1354a198acdf4fa1ed07e2163d Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 2 Dec 2022 11:53:07 +0800 Subject: [PATCH 11/12] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20=20command?= =?UTF-8?q?=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/__init__.py | 1 + apps/acls/api/command_acl.py | 10 +++++++-- apps/acls/serializers/__init__.py | 1 + apps/acls/serializers/command_filter.py | 16 +++++++++----- apps/acls/serializers/login_acl.py | 28 ++++++++---------------- apps/acls/serializers/login_asset_acl.py | 6 ++--- apps/acls/urls/api_urls.py | 4 ++-- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/apps/acls/api/__init__.py b/apps/acls/api/__init__.py index ff52a1ce9..2f720effe 100644 --- a/apps/acls/api/__init__.py +++ b/apps/acls/api/__init__.py @@ -1,3 +1,4 @@ +from .command_acl import * from .login_acl import * from .login_asset_acl import * from .login_asset_check import * diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py index 563e0c1a2..bb9a5482e 100644 --- a/apps/acls/api/command_acl.py +++ b/apps/acls/api/command_acl.py @@ -1,12 +1,18 @@ from orgs.mixins.api import OrgBulkModelViewSet from .. import models, serializers - __all__ = ['CommandFilterACLViewSet'] +class CommandGroupViewSet(OrgBulkModelViewSet): + model = models.CommandGroup + filterset_fields = ('name',) + search_fields = filterset_fields + serializer_class = serializers.CommandGroupSerializer + + class CommandFilterACLViewSet(OrgBulkModelViewSet): model = models.CommandFilterACL - filterset_fields = ('name', ) + filterset_fields = ('name',) search_fields = filterset_fields serializer_class = serializers.LoginAssetACLSerializer diff --git a/apps/acls/serializers/__init__.py b/apps/acls/serializers/__init__.py index ff52a1ce9..465474c4f 100644 --- a/apps/acls/serializers/__init__.py +++ b/apps/acls/serializers/__init__.py @@ -1,3 +1,4 @@ +from .command_filter import * from .login_acl import * from .login_asset_acl import * from .login_asset_check import * diff --git a/apps/acls/serializers/command_filter.py b/apps/acls/serializers/command_filter.py index a6b090c42..934d38198 100644 --- a/apps/acls/serializers/command_filter.py +++ b/apps/acls/serializers/command_filter.py @@ -3,14 +3,20 @@ from django.utils.translation import ugettext_lazy as _ from acls.models import CommandGroup, CommandFilterACL from common.drf.fields import ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .base import BaseUserAssetAccountACLSerializerMixin +from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer -__all__ = ["CommandFilterACLSerializer"] +__all__ = ["CommandFilterACLSerializer", "CommandGroupSerializer"] -class CommandFilterACLSerializer(BaseUserAssetAccountACLSerializerMixin, BulkOrgResourceModelSerializer): +class CommandGroupSerializer(BulkOrgResourceModelSerializer): + class Meta: + model = CommandGroup + fields = ['id', 'name', 'type', 'content', 'comment'] + + +class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer): commands = ObjectRelatedField(queryset=CommandGroup.objects, many=True, required=False, label=_('Commands')) - class Meta(BaseUserAssetAccountACLSerializerMixin.Meta): + class Meta(BaseSerializer.Meta): model = CommandFilterACL - fields = BaseUserAssetAccountACLSerializerMixin.Meta.fields + ['commands'] + fields = BaseSerializer.Meta.fields + ['commands'] diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index f759da435..db89445c8 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.drf.serializers import BulkModelSerializer -from common.drf.serializers import MethodSerializer -from common.drf.fields import ObjectRelatedField + +from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.drf.serializers import BulkModelSerializer, MethodSerializer from jumpserver.utils import has_valid_xpack_license from users.models import User -from ..models import LoginACL from .rules import RuleSerializer +from ..models import LoginACL __all__ = [ "LoginACLSerializer", @@ -22,9 +22,7 @@ class LoginACLSerializer(BulkModelSerializer): reviewers = ObjectRelatedField( queryset=User.objects, label=_("Reviewers"), many=True, required=False ) - action_display = serializers.ReadOnlyField( - source="get_action_display", label=_("Action") - ) + action = LabeledChoiceField(choices=LoginACL.ActionChoices.choices) reviewers_amount = serializers.IntegerField( read_only=True, source="reviewers.count" ) @@ -34,17 +32,9 @@ class LoginACLSerializer(BulkModelSerializer): model = LoginACL fields_mini = ["id", "name"] fields_small = fields_mini + [ - "priority", - "rules", - "action", - "action_display", - "is_active", - "user", - "date_created", - "date_updated", - "reviewers_amount", - "comment", - "created_by", + "priority", "user", "rules", "action", + "is_active", "date_created", "date_updated", + "reviewers_amount", "comment", "created_by", ] fields_fk = ["user"] fields_m2m = ["reviewers"] @@ -65,7 +55,7 @@ class LoginACLSerializer(BulkModelSerializer): return choices = action._choices if not has_valid_xpack_license(): - choices.pop(LoginACL.ActionChoices.confirm, None) + choices.pop(LoginACL.ActionChoices.review, None) action._choices = choices def get_rules_serializer(self): diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index b360bb55f..de160d124 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -1,11 +1,11 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .base import BaseUserAssetAccountACLSerializerMixin +from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer from ..models import LoginAssetACL __all__ = ["LoginAssetACLSerializer"] -class LoginAssetACLSerializer(BaseUserAssetAccountACLSerializerMixin, BulkOrgResourceModelSerializer): - class Meta(BaseUserAssetAccountACLSerializerMixin.Meta): +class LoginAssetACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer): + class Meta(BaseSerializer.Meta): model = LoginAssetACL diff --git a/apps/acls/urls/api_urls.py b/apps/acls/urls/api_urls.py index c4040ff45..742082022 100644 --- a/apps/acls/urls/api_urls.py +++ b/apps/acls/urls/api_urls.py @@ -1,14 +1,14 @@ from django.urls import path from rest_framework_bulk.routes import BulkRouter + from .. import api - app_name = 'acls' - router = BulkRouter() router.register(r'login-acls', api.LoginACLViewSet, 'login-acl') router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl') +router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl') urlpatterns = [ path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'), From 61e6ab20a2e8f46981c4f5e11cdb84ae30b2c4f5 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 2 Dec 2022 12:27:26 +0800 Subject: [PATCH 12/12] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20Connect=20ac?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/command_acl.py | 2 +- apps/acls/migrations/0004_connectacl.py | 40 ------------------- .../migrations/0008_commandgroup_comment.py | 18 +++++++++ apps/acls/models/command_acl.py | 1 + apps/acls/urls/api_urls.py | 1 + .../0016_alter_userloginlog_type.py | 20 ---------- 6 files changed, 21 insertions(+), 61 deletions(-) delete mode 100644 apps/acls/migrations/0004_connectacl.py create mode 100644 apps/acls/migrations/0008_commandgroup_comment.py delete mode 100644 apps/audits/migrations/0016_alter_userloginlog_type.py diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py index bb9a5482e..b717a464f 100644 --- a/apps/acls/api/command_acl.py +++ b/apps/acls/api/command_acl.py @@ -1,7 +1,7 @@ from orgs.mixins.api import OrgBulkModelViewSet from .. import models, serializers -__all__ = ['CommandFilterACLViewSet'] +__all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet'] class CommandGroupViewSet(OrgBulkModelViewSet): diff --git a/apps/acls/migrations/0004_connectacl.py b/apps/acls/migrations/0004_connectacl.py deleted file mode 100644 index 3b640d1f3..000000000 --- a/apps/acls/migrations/0004_connectacl.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 3.2.16 on 2022-11-30 02:46 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0040_alter_user_source'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('acls', '0003_auto_20211130_1037'), - ] - - operations = [ - migrations.CreateModel( - name='ConnectACL', - fields=[ - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), - ('is_active', models.BooleanField(default=True, verbose_name='Active')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('rules', models.JSONField(default=list, verbose_name='Rule')), - ('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')), - ('user_groups', models.ManyToManyField(blank=True, related_name='connect_acls', to='users.UserGroup', verbose_name='User group')), - ('users', models.ManyToManyField(blank=True, related_name='connect_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'Connect acl', - 'ordering': ('priority', '-date_updated', 'name'), - }, - ), - ] diff --git a/apps/acls/migrations/0008_commandgroup_comment.py b/apps/acls/migrations/0008_commandgroup_comment.py new file mode 100644 index 000000000..631ff8eb7 --- /dev/null +++ b/apps/acls/migrations/0008_commandgroup_comment.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-12-02 04:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('acls', '0007_auto_20221202_1048'), + ] + + operations = [ + migrations.AddField( + model_name='commandgroup', + name='comment', + field=models.TextField(blank=True, verbose_name='Comment'), + ), + ] diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index 056d9b4f6..bec473250 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -24,6 +24,7 @@ class CommandGroup(JMSOrgBaseModel): type = models.CharField(max_length=16, default=Type.command, choices=Type.choices, verbose_name=_("Type")) content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case')) + comment = models.TextField(blank=True, verbose_name=_("Comment")) class Meta: unique_together = [('org_id', 'name')] diff --git a/apps/acls/urls/api_urls.py b/apps/acls/urls/api_urls.py index 742082022..0185278d9 100644 --- a/apps/acls/urls/api_urls.py +++ b/apps/acls/urls/api_urls.py @@ -8,6 +8,7 @@ app_name = 'acls' router = BulkRouter() router.register(r'login-acls', api.LoginACLViewSet, 'login-acl') router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl') +router.register(r'command-groups', api.CommandGroupViewSet, 'command-group') router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl') urlpatterns = [ diff --git a/apps/audits/migrations/0016_alter_userloginlog_type.py b/apps/audits/migrations/0016_alter_userloginlog_type.py deleted file mode 100644 index 1047a4a30..000000000 --- a/apps/audits/migrations/0016_alter_userloginlog_type.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.16 on 2022-11-30 07:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('audits', '0015_auto_20221111_1919'), - ] - - operations = [ - migrations.AlterField( - model_name='userloginlog', - name='type', - field=models.CharField(choices=[('web_cli', 'Web Client'), ('web_gui', 'Web GUI'), ('db_cli', 'DB Client'), - ('db_gui', 'DB GUI'), ('rdp_cli', 'RDP Client'), ('rdp_file', 'RDP File'), - ('ssh_cli', 'SSH Client'), ('web_sftp', 'Web SFTP')], max_length=128, - verbose_name='Login type'), - ), - ]