Compare commits

...

74 Commits
v3.10.14 ... v3

Author SHA1 Message Date
feng
f83eeeb568 perf: Top session asset user cache 2025-12-23 18:01:40 +08:00
fit2bot
7ab5b3efab fix: update poetry.lock (#16471)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-23 17:26:45 +08:00
Eric_Lee
81254be293 perf: update rdp params 2025-12-23 16:51:25 +08:00
fit2bot
d5df73e1ad fix: Account backup: when sending to the mailbox fails, the task status also shows the success problem (#16460)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-22 16:03:05 +08:00
fit2bot
a497b3cf94 fix: Add '/media/' to the list of whitelisted URLs for MFA login (#16412)
Co-authored-by: wangruidong <940853815@qq.com>
2025-12-10 14:19:39 +08:00
fit2bot
8548b73063 fix: Failed to switch languages (#16326)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-24 11:13:56 +08:00
fit2bot
182320f492 fix: SAML2 authentication failure with Okta integration (#16250)
Co-authored-by: wangruidong <940853815@qq.com>
2025-11-07 15:42:09 +08:00
fit2bot
40d326d6a6 fix: Any change to the LDAP server URI should require re-authentication and explicit re-entry of (#16195)
the bind password, not reuse stored credentials

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

* fix: Prompt message

---------

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

* perf: modify url

---------

Co-authored-by: Eric <xplzv@126.com>
2024-12-11 11:42:34 +08:00
Bai
9cd163c99d fix: when oidc enabled and use_state user login raise 400 2024-12-06 16:26:59 +08:00
wangruidong
e72073f0cc perf: Add viewAssetOnlineSessionInfo conf 2024-11-25 15:26:14 +08:00
wangruidong
690f525afc perf: Add check for SECURITY_COMMAND_EXECUTION settings in ops tasks 2024-11-11 18:15:11 +08:00
110 changed files with 4679 additions and 2975 deletions

View File

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

View File

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

View File

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

View File

@@ -52,6 +52,7 @@
password: "{{ account.secret | password_hash('des') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -68,6 +69,7 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: Refresh connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,5 +101,7 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

@@ -52,6 +52,7 @@
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -68,6 +69,7 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: Refresh connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,5 +101,7 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,6 +52,7 @@
password: "{{ account.secret | password_hash('des') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -68,6 +69,7 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: Refresh connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,6 +101,8 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

@@ -52,6 +52,7 @@
password: "{{ account.secret | password_hash('sha512') }}"
update_password: always
ignore_errors: true
register: change_secret_result
when: account.secret_type == "password"
- name: remove jumpserver ssh key
@@ -68,6 +69,7 @@
user: "{{ account.username }}"
key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key"
- name: Refresh connection
@@ -86,7 +88,9 @@
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
when:
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -97,6 +101,8 @@
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
when:
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ def create_accounts_activities(account, action='create'):
@receiver(post_save, sender=Account)
def on_account_create_by_template(sender, instance, created=False, **kwargs):
if not created or instance.source != Source.TEMPLATE:
if not created:
return
push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,8 @@ class HostTypes(BaseType):
'gather_accounts_enabled': True,
'verify_account_enabled': True,
'change_secret_enabled': True,
'push_account_enabled': True
'push_account_enabled': True,
'remove_account_enabled': True,
},
cls.WINDOWS: {
'ansible_config': {

View File

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

View File

@@ -27,6 +27,7 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
"change_secret_enabled", "change_secret_method", "change_secret_params",
"verify_account_enabled", "verify_account_method", "verify_account_params",
"gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
"remove_account_enabled", "remove_account_method", "remove_account_params",
]
extra_kwargs = {
# 启用资产探测
@@ -42,6 +43,8 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
"push_account_method": {"label": _("Push account method")},
"gather_accounts_enabled": {"label": _("Gather accounts enabled")},
"gather_accounts_method": {"label": _("Gather accounts method")},
"remove_account_method": {"label": _("Remove account method")},
"remove_account_enabled": {"label": _("Remove account enabled")},
}

View File

@@ -189,9 +189,13 @@ class ResourceActivityAPIView(generics.ListAPIView):
'id', 'datetime', 'r_detail', 'r_detail_id',
'r_user', 'r_action', 'r_type'
)
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
org_q = Q()
if not current_org.is_root():
org_q = Q(org_id=Organization.SYSTEM_ID) | Q(org_id=current_org.id)
if resource_id:
org_q |= Q(org_id='') | Q(org_id=Organization.ROOT_ID)
with tmp_to_root_org():
qs1 = self.get_operate_log_qs(fields, limit, org_q, resource_id=resource_id)
qs2 = self.get_activity_log_qs(fields, limit, org_q, resource_id=resource_id)

View File

@@ -14,7 +14,7 @@ from audits.handler import (
create_or_update_operate_log, get_instance_dict_from_cache
)
from audits.utils import model_to_dict_for_operate_log as model_to_dict
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, OP_LOG_SKIP_SIGNAL
from common.signals import django_ready
from jumpserver.utils import current_request
from ..const import MODELS_NEED_RECORD, ActionChoices
@@ -77,7 +77,7 @@ def signal_of_operate_log_whether_continue(
condition = True
if not instance:
condition = False
if instance and getattr(instance, SKIP_SIGNAL, False):
if instance and getattr(instance, OP_LOG_SKIP_SIGNAL, False):
condition = False
# 不记录组件的操作日志
user = current_request.user if current_request else None

View File

@@ -66,6 +66,8 @@ class RDPFileClientProtocolURLMixin:
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'bitmapcachepersistenable:i': '0',
'bitmapcachesize:i': '1500',
}
# 设置多屏显示
multi_mon = is_true(self.request.query_params.get('multi_mon'))
@@ -472,6 +474,8 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'list': 'authentication.view_superconnectiontoken',
'retrieve': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -479,7 +483,12 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
}
def get_queryset(self):
return ConnectionToken.objects.all()
return ConnectionToken.objects.none()
def get_object(self):
pk = self.kwargs.get(self.lookup_field)
token = get_object_or_404(ConnectionToken, pk=pk)
return token
def get_user(self, serializer):
return serializer.validated_data.get('user')

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ class MFAMiddleware:
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
white_urls = [
'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
'/profile/otp', '/logout/',
'/profile/otp', '/logout/', '/media/'
]
for url in white_urls:
if request.path.find(url) > -1:
@@ -77,6 +77,7 @@ class ThirdPartyLoginMiddleware(mixins.AuthMixin):
ip = get_request_ip(request)
try:
self.request = request
self.check_is_block()
self._check_third_party_login_acl()
self._check_login_acl(request.user, ip)
except Exception as e:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
pk_number = struct.pack('!B', 1)
registered_delivery = struct.pack('!B', 0)
msg_level = struct.pack('!B', 0)
service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
service_id = service_id.ljust(10, '\x00').encode('utf-8')
fee_user_type = struct.pack('!B', 2)
fee_terminal_id = ('0' * 21).encode('utf-8')
tp_pid = struct.pack('!B', 0)
@@ -85,7 +85,7 @@ class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
fee_code = '000000'.encode('utf-8')
valid_time = ('\x00' * 17).encode('utf-8')
at_time = ('\x00' * 17).encode('utf-8')
src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
src_id = src_id.ljust(21, '\x00').encode('utf-8')
reserve = b'\x00' * 8
_msg_length = struct.pack('!B', len(msg_content) * 2)
_msg_src = msg_src.encode('utf-8')

View File

@@ -1,10 +1,10 @@
import os
import jms_storage
from celery import shared_task
from django.conf import settings
from django.core.mail import send_mail, EmailMultiAlternatives, get_connection
from django.utils.translation import gettext_lazy as _
import jms_storage
from .utils import get_logger
@@ -71,10 +71,7 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list
for attachment in attachment_list:
email.attach_file(attachment)
os.remove(attachment)
try:
return email.send()
except Exception as e:
logger.error("Sending mail attachment error: {}".format(e))
return email.send()
@shared_task(verbose_name=_('Upload session replay to external storage'))

View File

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

View File

@@ -145,6 +145,7 @@ class DateTimeMixin:
class DatesLoginMetricMixin:
days: int
dates_list: list
date_start_end: tuple
command_type_queryset_tuple: tuple
@@ -156,6 +157,8 @@ class DatesLoginMetricMixin:
operate_logs_queryset: OperateLog.objects
password_change_logs_queryset: PasswordChangeLog.objects
CACHE_TIMEOUT = 60
@lazyproperty
def get_type_to_assets(self):
result = Asset.objects.annotate(type=F('platform__type')). \
@@ -215,19 +218,34 @@ class DatesLoginMetricMixin:
return date_metrics_dict.get('id', [])
def get_dates_login_times_assets(self):
cache_key = f"stats:top10_assets:{self.days}"
data = cache.get(cache_key)
if data is not None:
return data
assets = self.sessions_queryset.values("asset") \
.annotate(total=Count("asset")) \
.annotate(last=Cast(Max("date_start"), output_field=CharField())) \
.order_by("-total")
return list(assets[:10])
result = list(assets[:10])
cache.set(cache_key, result, self.CACHE_TIMEOUT)
return result
def get_dates_login_times_users(self):
cache_key = f"stats:top10_users:{self.days}"
data = cache.get(cache_key)
if data is not None:
return data
users = self.sessions_queryset.values("user_id") \
.annotate(total=Count("user_id")) \
.annotate(user=Max('user')) \
.annotate(last=Cast(Max("date_start"), output_field=CharField())) \
.order_by("-total")
return list(users[:10])
result = list(users[:10])
cache.set(cache_key, result, self.CACHE_TIMEOUT)
return result
def get_dates_login_record_sessions(self):
sessions = self.sessions_queryset.order_by('-date_start')

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,8 @@ TERMINAL_REPLAY_STORAGE = CONFIG.TERMINAL_REPLAY_STORAGE
FTP_FILE_MAX_STORE = CONFIG.FTP_FILE_MAX_STORE
# Security settings
CHECK_CONN_AFTER_CHANGE = CONFIG.CHECK_CONN_AFTER_CHANGE
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY = CONFIG.SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
@@ -115,7 +117,9 @@ EMAIL_CUSTOM_USER_CREATED_BODY = CONFIG.EMAIL_CUSTOM_USER_CREATED_BODY
EMAIL_CUSTOM_USER_CREATED_SIGNATURE = CONFIG.EMAIL_CUSTOM_USER_CREATED_SIGNATURE
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
DEFAULT_EXPIRED_YEARS = 70
DEFAULT_EXPIRED_YEARS = CONFIG.DEFAULT_EXPIRED_YEARS
USER_DEFAULT_EXPIRED_DAYS = CONFIG.USER_DEFAULT_EXPIRED_DAYS
ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS = CONFIG.ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS
USER_GUIDE_URL = CONFIG.USER_GUIDE_URL
HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT

View File

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

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
#
import re
from urllib.parse import urlparse
from django.conf import settings
from django.http import HttpResponse
from django.http import HttpResponseBadRequest
from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
@@ -16,7 +18,7 @@ from common.views.http import HttpResponseTemporaryRedirect
__all__ = [
'LunaView', 'I18NView', 'KokoView', 'WsView',
'redirect_format_api', 'redirect_old_apps_view', 'UIView',
'ResourceDownload',
'ResourceDownload', 'RedirectConfirm'
]
@@ -94,3 +96,24 @@ def csrf_failure(request, reason=""):
from django.shortcuts import reverse
login_url = reverse('authentication:login') + '?csrf_failure=1&admin=1'
return redirect(login_url)
class RedirectConfirm(TemplateView):
template_name = 'redirect_confirm.html'
def get(self, request, *args, **kwargs):
next_url = self.request.GET.get("next")
if not self.is_valid_url(next_url):
return HttpResponseBadRequest("Invalid next url")
return self.render_to_response({"target_url": next_url})
@staticmethod
def is_valid_url(url):
if not url:
return False
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
return False
if parsed.scheme not in ['http', 'https']:
return False
return True

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4517c6a7464c68f949912b97c8a9abcc766ca19e32267a1d1da3f0e012471c1a
size 146255
oid sha256:9d5e378a6e129625a1cc2b7b043e8feb3a48b0c239239d0ab954525b51244969
size 146503

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d4d2709e597e055072474f08be2f363d43df239240051024a77213ba48ecfac
size 146366
oid sha256:884dde4efaa8a8a80efee337248c6a7c1ff5fc83e486e34db685a1d20479e109
size 146115

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ from .base import BackendBase
class Email(BackendBase):
account_field = 'email'
is_enable_field_in_settings = 'EMAIL_HOST_USER'
is_enable_field_in_settings = 'EMAIL_HOST'
def send_msg(self, users, message, subject):
accounts, __, __ = self.get_accounts(users)

View File

@@ -7,10 +7,12 @@ from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import PeriodicTask
from django.conf import settings
from common.const.crontab import CRONTAB_AT_AM_TWO
from common.utils import get_logger, get_object_or_none, get_log_keep_day
from ops.celery import app
from ops.const import Types
from orgs.utils import tmp_to_org, tmp_to_root_org
from .celery.decorator import (
register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic
@@ -56,7 +58,8 @@ def run_ops_job(job_id):
if not job:
logger.error("Did not get the execution: {}".format(job_id))
return
if not settings.SECURITY_COMMAND_EXECUTION and job.type != Types.upload_file:
return
with tmp_to_org(job.org):
execution = job.create_execution()
execution.creator = job.creator
@@ -83,6 +86,8 @@ def run_ops_job_execution(execution_id, **kwargs):
if not execution:
logger.error("Did not get the execution: {}".format(execution_id))
return
if not settings.SECURITY_COMMAND_EXECUTION and execution.job.type != Types.upload_file:
return
_run_ops_job_execution(execution)

30
apps/orgs/mixins/ws.py Normal file
View File

@@ -0,0 +1,30 @@
from http.cookies import SimpleCookie
from asgiref.sync import sync_to_async
from orgs.utils import tmp_to_org
class OrgMixin:
cookie = None
org = None
def get_cookie(self):
try:
headers = self.scope['headers']
headers_dict = {key.decode('utf-8'): value.decode('utf-8') for key, value in headers}
cookie = SimpleCookie(headers_dict.get('cookie', ''))
except Exception as e:
cookie = SimpleCookie()
return cookie
def get_current_org(self):
oid = self.cookie.get('X-JMS-ORG')
return oid.value if oid else None
@sync_to_async
def has_perms(self, user, perms):
self.cookie = self.get_cookie()
self.org = self.get_current_org()
with tmp_to_org(self.org):
return user.has_perms(perms)

View File

@@ -38,7 +38,7 @@ class OrgSerializer(ModelSerializer):
class CurrentOrgSerializer(ModelSerializer):
class Meta:
model = Organization
fields = ['id', 'name', 'is_default', 'is_root', 'comment']
fields = ['id', 'name', 'is_default', 'is_root', 'is_system', 'comment']
class CurrentOrgDefault:

View File

@@ -5,8 +5,8 @@ from contextlib import contextmanager
from functools import wraps
from inspect import signature
from werkzeug.local import LocalProxy
from django.conf import settings
from werkzeug.local import LocalProxy
from common.local import thread_local
from .models import Organization
@@ -24,13 +24,13 @@ def get_org_from_request(request):
# 其次session
if not oid:
oid = request.session.get("oid")
if oid and oid.lower() == 'default':
return Organization.default()
if oid and oid.lower() == 'root':
return Organization.root()
if oid and oid.lower() == 'system':
return Organization.system()
@@ -39,14 +39,14 @@ def get_org_from_request(request):
if org and org.internal:
# 内置组织直接返回
return org
if not settings.XPACK_ENABLED:
# 社区版用户只能使用默认组织
return Organization.default()
if not org and request.user.is_authenticated:
# 企业版用户优先从自己有权限的组织中获取
org = request.user.orgs.first()
org = request.user.orgs.exclude(id=Organization.SYSTEM_ID).first()
if not org:
org = Organization.default()
@@ -57,6 +57,8 @@ def get_org_from_request(request):
def set_current_org(org):
if isinstance(org, (str, uuid.UUID)):
org = Organization.get_instance(org)
if not org:
return
setattr(thread_local, 'current_org_id', org.id)
@@ -97,20 +99,24 @@ def get_current_org_id_for_serializer():
@contextmanager
def tmp_to_root_org():
ori_org = get_current_org()
set_to_root_org()
yield
if ori_org is not None:
set_current_org(ori_org)
try:
set_to_root_org()
yield
finally:
if ori_org is not None:
set_current_org(ori_org)
@contextmanager
def tmp_to_org(org):
ori_org = get_current_org()
if org:
set_current_org(org)
yield
if ori_org is not None:
set_current_org(ori_org)
try:
if org:
set_current_org(org)
yield
finally:
if ori_org is not None:
set_current_org(ori_org)
@contextmanager
@@ -121,11 +127,13 @@ def tmp_to_builtin_org(system=0, default=0):
org_id = Organization.DEFAULT_ID
else:
raise ValueError("Must set system or default")
ori_org = get_current_org()
set_current_org(org_id)
yield
if ori_org is not None:
set_current_org(ori_org)
try:
ori_org = get_current_org()
set_current_org(org_id)
yield
finally:
if ori_org is not None:
set_current_org(ori_org)
def filter_org_queryset(queryset):

View File

@@ -3,13 +3,17 @@
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework.request import Request
from django.core.cache import cache
from common.exceptions import JMSObjectDoesNotExist
from common.utils import is_uuid
from common.utils.http import is_true
from common.utils import lazyproperty, is_uuid
from rbac.permissions import RBACPermission
from users.models import User
from perms.utils import UserPermTreeRefreshUtil
__all__ = ['SelfOrPKUserMixin']
__all__ = ['SelfOrPKUserMixin', 'RebuildTreeMixin']
class SelfOrPKUserMixin:
@@ -57,3 +61,33 @@ class SelfOrPKUserMixin:
def request_user_is_self(self):
return self.kwargs.get('user') in ['my', 'self']
class RebuildTreeMixin:
user: User
request: Request
def get(self, request, *args, **kwargs):
UserPermTreeRefreshUtil(self.user).refresh_if_need(force=self.is_force_refresh_tree)
return super().get(request, *args, **kwargs)
@lazyproperty
def is_force_refresh_tree(self):
force = is_true(self.request.query_params.get('rebuild_tree'))
if not force:
force = self.compute_is_force_refresh()
return force
def compute_is_force_refresh(self):
""" 5s 内连续刷新三次转为强制刷新 """
force_timeout = 5
force_max_count = 3
force_cache_key = '{user_id}:{path}'.format(user_id=self.user.id, path=self.request.path)
count = cache.get(force_cache_key, 1)
if count >= force_max_count:
force = True
cache.delete(force_cache_key)
else:
force = False
cache.set(force_cache_key, count + 1, force_timeout)
return force

View File

@@ -8,7 +8,7 @@ from assets.models import Node
from common.utils import get_logger, lazyproperty
from perms import serializers
from perms.utils import UserPermNodeUtil
from .mixin import SelfOrPKUserMixin
from .mixin import SelfOrPKUserMixin, RebuildTreeMixin
logger = get_logger(__name__)
@@ -18,7 +18,7 @@ __all__ = [
]
class BaseUserPermedNodesApi(SelfOrPKUserMixin, ListAPIView):
class BaseUserPermedNodesApi(SelfOrPKUserMixin, RebuildTreeMixin, ListAPIView):
serializer_class = serializers.NodePermedSerializer
def get_queryset(self):

View File

@@ -4,7 +4,7 @@ from rest_framework.response import Response
from assets.api import SerializeToTreeNodeMixin
from assets.models import Asset
from common.utils import get_logger
from .mixin import RebuildTreeMixin
from ..mixin import RebuildTreeMixin
from ..assets import UserAllPermedAssetsApi
logger = get_logger(__name__)

View File

@@ -1,40 +0,0 @@
from django.core.cache import cache
from rest_framework.request import Request
from common.utils.http import is_true
from common.utils import lazyproperty
from perms.utils import UserPermTreeRefreshUtil
from users.models import User
__all__ = ['RebuildTreeMixin']
class RebuildTreeMixin:
user: User
request: Request
def get(self, request, *args, **kwargs):
UserPermTreeRefreshUtil(self.user).refresh_if_need(force=self.is_force_refresh_tree)
return super().get(request, *args, **kwargs)
@lazyproperty
def is_force_refresh_tree(self):
force = is_true(self.request.query_params.get('rebuild_tree'))
if not force:
force = self.compute_is_force_refresh()
return force
def compute_is_force_refresh(self):
""" 5s 内连续刷新三次转为强制刷新 """
force_timeout = 5
force_max_count = 3
force_cache_key = '{user_id}:{path}'.format(user_id=self.user.id, path=self.request.path)
count = cache.get(force_cache_key, 1)
if count >= force_max_count:
force = True
cache.delete(force_cache_key)
else:
force = False
cache.set(force_cache_key, count + 1, force_timeout)
return force

View File

@@ -3,7 +3,6 @@ from rest_framework.response import Response
from assets.api import SerializeToTreeNodeMixin
from common.utils import get_logger
from .mixin import RebuildTreeMixin
from ..nodes import (
UserAllPermedNodesApi,
UserPermedNodeChildrenApi,
@@ -17,7 +16,7 @@ __all__ = [
]
class NodeTreeMixin(RebuildTreeMixin, SerializeToTreeNodeMixin):
class NodeTreeMixin(SerializeToTreeNodeMixin):
filter_queryset: callable
get_queryset: callable

View File

@@ -21,8 +21,7 @@ from perms.hands import Node
from perms.models import PermNode
from perms.utils import PermAssetDetailUtil, UserPermNodeUtil
from perms.utils import UserPermAssetUtil
from .mixin import RebuildTreeMixin
from ..mixin import SelfOrPKUserMixin
from ..mixin import SelfOrPKUserMixin, RebuildTreeMixin
__all__ = [
'UserGrantedK8sAsTreeApi',
@@ -186,6 +185,11 @@ class UserPermedNodeChildrenWithAssetsAsCategoryTreeApi(BaseUserNodeWithAssetAsT
return [], []
if not self.tp or not all(self.tp):
nodes = UserPermAssetUtil.get_type_nodes_tree_or_cached(self.user)
if self.request.query_params.get('count_resource'):
# 解决在 lina 使用该 api 类型树套娃问题
for node in nodes:
if node.get('meta'):
node['isParent'] = False
return nodes, []
category, tp = self.tp

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.1.13 on 2025-08-25 03:03
import common.utils.django
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('perms', '0036_auto_20231108_1626'),
]
operations = [
migrations.AlterField(
model_name='assetpermission',
name='date_expired',
field=models.DateTimeField(db_index=True, default=common.utils.django.asset_permission_date_expired_default, verbose_name='Date expired'),
),
]

View File

@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from accounts.const import AliasAccount
from accounts.models import Account
from assets.models import Asset
from common.utils import date_expired_default, lazyproperty
from common.utils import asset_permission_date_expired_default, lazyproperty
from common.utils.timezone import local_now
from labels.mixins import LabeledMixin
from orgs.mixins.models import JMSOrgBaseModel
@@ -77,7 +77,7 @@ class AssetPermission(LabeledMixin, JMSOrgBaseModel):
actions = models.IntegerField(default=ActionChoices.connect, verbose_name=_("Actions"))
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start"))
date_expired = models.DateTimeField(
default=date_expired_default, db_index=True, verbose_name=_('Date expired')
default=asset_permission_date_expired_default, db_index=True, verbose_name=_('Date expired')
)
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket'))

View File

@@ -105,6 +105,9 @@ class AssetPermissionSerializer(ResourceLabelsMixin, BulkOrgResourceModelSeriali
def create_account_through_template(self, nodes, assets):
if not self.template_accounts:
return
if self.instance:
assets = assets or self.instance.assets.all()
nodes = nodes or self.instance.nodes.all()
assets = self.get_all_assets(nodes, assets)
self.create_accounts(assets)

View File

@@ -49,6 +49,7 @@ def check_asset_permission_will_expired():
org_perm_remain_day_mapper = defaultdict(dict)
asset_perms = AssetPermission.objects.filter(
is_active=True,
date_expired__gte=start,
date_expired__lte=end
).distinct()

View File

@@ -24,7 +24,7 @@ exclude_permissions = (
('authentication', 'privatetoken', '*', '*'),
('authentication', 'connectiontoken', 'delete,change', 'connectiontoken'),
('authentication', 'connectiontoken', 'view', 'connectiontokensecret'),
('authentication', 'ssotoken', '*', '*'),
('authentication', 'ssotoken', 'change,delete', 'ssotoken'),
('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'),
('authentication', 'temptoken', 'delete', 'temptoken'),
('users', 'userpasswordhistory', '*', '*'),
@@ -148,6 +148,7 @@ only_system_permissions = (
('authentication', 'superconnectiontoken', '*', '*'),
('authentication', 'temptoken', '*', '*'),
('authentication', 'passkey', '*', '*'),
('authentication', 'ssotoken', '*', '*'),
('tickets', '*', '*', '*'),
('orgs', 'organization', 'view', 'rootorg'),
('terminal', 'applet', '*', '*'),

View File

@@ -9,7 +9,8 @@ from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
from common.utils import signer, get_logger
from common.utils import get_logger
from common.db.utils import Encryptor
logger = get_logger(__name__)
@@ -51,7 +52,7 @@ class Setting(models.Model):
try:
value = self.value
if self.encrypted:
value = signer.unsign(value)
value = Encryptor(value).decrypt()
if not value:
return None
value = json.loads(value)
@@ -64,7 +65,7 @@ class Setting(models.Model):
try:
v = json.dumps(item)
if self.encrypted:
v = signer.sign(v)
v = Encryptor(v).encrypt()
self.value = v
except json.JSONDecodeError as e:
raise ValueError("Json dump error: {}".format(str(e)))

View File

@@ -30,6 +30,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField()
SECURITY_WATERMARK_ENABLED = serializers.BooleanField()
SESSION_EXPIRE_AT_BROWSER_CLOSE = serializers.BooleanField()
VIEW_ASSET_ONLINE_SESSION_INFO = serializers.BooleanField()
PASSWORD_RULE = serializers.DictField()
SECURITY_SESSION_SHARE = serializers.BooleanField()
XPACK_LICENSE_IS_VALID = serializers.BooleanField()
@@ -63,6 +64,9 @@ class PrivateSettingSerializer(PublicSettingSerializer):
GPT_MODEL = serializers.CharField()
FILE_UPLOAD_SIZE_LIMIT_MB = serializers.IntegerField()
DEFAULT_EXPIRED_YEARS = serializers.IntegerField()
USER_DEFAULT_EXPIRED_DAYS = serializers.IntegerField()
ASSET_PERMISSION_DEFAULT_EXPIRED_DAYS = serializers.IntegerField()
class ServerInfoSerializer(serializers.Serializer):
CURRENT_TIME = serializers.DateTimeField()

View File

@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
#
import json
import asyncio
import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.core.cache import cache
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from common.db.utils import close_old_connections
from common.utils import get_logger
from orgs.mixins.ws import OrgMixin
from orgs.models import Organization
from orgs.utils import current_org
from settings.serializers import (
LDAPTestConfigSerializer,
LDAPTestLoginSerializer
)
from orgs.models import Organization
from orgs.utils import current_org
from settings.tasks import sync_ldap_user
from settings.utils import (
LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil,
@@ -97,10 +98,10 @@ class ToolsWebsocket(AsyncJsonWebsocketConsumer):
close_old_connections()
class LdapWebsocket(AsyncJsonWebsocketConsumer):
class LdapWebsocket(AsyncJsonWebsocketConsumer, OrgMixin):
async def connect(self):
user = self.scope["user"]
if user.is_authenticated:
if user.is_authenticated and await self.has_perms(user, ['settings.view_setting']):
await self.accept()
else:
await self.close()
@@ -133,7 +134,7 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
auth_ldap = serializer.validated_data.get('AUTH_LDAP', False)
if not password:
if not password and server_uri == settings.AUTH_LDAP_SERVER_URI:
password = settings.AUTH_LDAP_BIND_PASSWORD
config = {
@@ -193,16 +194,14 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
users = self.get_ldap_users(username_list, cache_police)
if users is None:
msg = _('Get ldap users is None')
orgs = self.get_orgs(org_ids)
new_users, error_msg = LDAPImportUtil().perform_import(users, orgs)
if error_msg:
msg = error_msg
count = users if users is None else len(users)
orgs_name = ', '.join([str(org) for org in orgs])
ok = True
msg = _('Imported {} users successfully (Organization: {})').format(count, orgs_name)
else:
orgs = self.get_orgs(org_ids)
_new_users, error_msg = LDAPImportUtil().perform_import(users, orgs)
ok = True
success_count = len(users) - len(error_msg)
msg = _('Total {}, success {}, failure {}').format(
len(users), success_count, len(error_msg)
)
except Exception as e:
msg = str(e)
return ok, msg

View File

@@ -5570,14 +5570,14 @@ body.body-small .vote-icon {
.small-chat-box .form-chat {
padding: 10px 10px;
}
/*
* metismenu - v2.0.2
* A jQuery menu plugin
* https://github.com/onokumus/metisMenu
*
* Made by Osman Nuri Okumus
* Under MIT License
*/
/*!
* metismenu https://github.com/onokumus/metismenu#readme
* A collapsible jQuery menu plugin
* @version 3.0.7
* @author Osman Nuri Okumus <onokumus@gmail.com> (https://github.com/onokumus)
* @license: MIT
*/
.metismenu .plus-minus,
.metismenu .plus-times {
float: right;
@@ -5589,49 +5589,57 @@ body.body-small .vote-icon {
.metismenu .glyphicon.arrow:before {
content: "\e079";
}
.metismenu .active > a > .glyphicon.arrow:before {
content: "\e114";
.metismenu .mm-active > a > .glyphicon.arrow:before {
content: "\e114";
}
.metismenu .fa.arrow:before {
content: "\f104";
}
.metismenu .active > a > .fa.arrow:before {
content: "\f107";
.metismenu .mm-active > a > .fa.arrow:before {
content: "\f107";
}
.metismenu .ion.arrow:before {
content: "\f3d2";
}
.metismenu .active > a > .ion.arrow:before {
content: "\f3d0";
.metismenu .mm-active > a > .ion.arrow:before {
content: "\f3d0";
}
.metismenu .fa.plus-minus:before,
.metismenu .fa.plus-times:before {
content: "\f067";
}
.metismenu .active > a > .fa.plus-times {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
.metismenu .mm-active > a > .fa.plus-times {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.metismenu .active > a > .fa.plus-minus:before {
content: "\f068";
.metismenu .mm-active > a > .fa.plus-minus:before {
content: "\f068";
}
.metismenu .collapse {
display: none;
.metismenu .mm-collapse {
display: none;
}
.metismenu .collapse.in {
display: block;
.metismenu .mm-collapse.in {
display: block;
}
.metismenu .collapsing {
position: relative;
height: 0;
overflow: hidden;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
-webkit-transition-duration: .35s;
transition-duration: .35s;
-webkit-transition-property: height, visibility;
transition-property: height, visibility;
.metismenu .mm-collapsing {
position: relative;
height: 0;
overflow: hidden;
-webkit-transition-timing-function: ease;
transition-timing-function: ease;
-webkit-transition-duration: .35s;
transition-duration: .35s;
-webkit-transition-property: height, visibility;
transition-property: height, visibility;
}
/*
* Usage:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,368 @@
/*!
* metismenu https://github.com/onokumus/metismenu#readme
* A collapsible jQuery menu plugin
* @version 3.0.7
* @author Osman Nuri Okumus <onokumus@gmail.com> (https://github.com/onokumus)
* @license: MIT
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('jquery')) :
typeof define === 'function' && define.amd ? define(['jquery'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.metisMenu = factory(global.$));
}(this, (function ($) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var $__default = /*#__PURE__*/_interopDefaultLegacy($);
const Util = (($) => { // eslint-disable-line no-shadow
const TRANSITION_END = 'transitionend';
const Util = { // eslint-disable-line no-shadow
TRANSITION_END: 'mmTransitionEnd',
triggerTransitionEnd(element) {
$(element).trigger(TRANSITION_END);
},
supportsTransitionEnd() {
return Boolean(TRANSITION_END);
},
};
function getSpecialTransitionEndEvent() {
return {
bindType: TRANSITION_END,
delegateType: TRANSITION_END,
handle(event) {
if ($(event.target).is(this)) {
return event
.handleObj
.handler
.apply(this, arguments); // eslint-disable-line prefer-rest-params
}
return undefined;
},
};
}
function transitionEndEmulator(duration) {
let called = false;
$(this).one(Util.TRANSITION_END, () => {
called = true;
});
setTimeout(() => {
if (!called) {
Util.triggerTransitionEnd(this);
}
}, duration);
return this;
}
function setTransitionEndSupport() {
$.fn.mmEmulateTransitionEnd = transitionEndEmulator; // eslint-disable-line no-param-reassign
// eslint-disable-next-line no-param-reassign
$.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent();
}
setTransitionEndSupport();
return Util;
})($__default['default']);
const NAME = 'metisMenu';
const DATA_KEY = 'metisMenu';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const JQUERY_NO_CONFLICT = $__default['default'].fn[NAME];
const TRANSITION_DURATION = 350;
const Default = {
toggle: true,
preventDefault: true,
triggerElement: 'a',
parentTrigger: 'li',
subMenu: 'ul',
};
const Event = {
SHOW: `show${EVENT_KEY}`,
SHOWN: `shown${EVENT_KEY}`,
HIDE: `hide${EVENT_KEY}`,
HIDDEN: `hidden${EVENT_KEY}`,
CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`,
};
const ClassName = {
METIS: 'metismenu',
ACTIVE: 'mm-active',
SHOW: 'mm-show',
COLLAPSE: 'mm-collapse',
COLLAPSING: 'mm-collapsing',
COLLAPSED: 'mm-collapsed',
};
class MetisMenu {
// eslint-disable-line no-shadow
constructor(element, config) {
this.element = element;
this.config = {
...Default,
...config,
};
this.transitioning = null;
this.init();
}
init() {
const self = this;
const conf = this.config;
const el = $__default['default'](this.element);
el.addClass(ClassName.METIS); // add metismenu class to element
el.find(`${conf.parentTrigger}.${ClassName.ACTIVE}`)
.children(conf.triggerElement)
.attr('aria-expanded', 'true'); // add attribute aria-expanded=true the trigger element
el.find(`${conf.parentTrigger}.${ClassName.ACTIVE}`)
.parents(conf.parentTrigger)
.addClass(ClassName.ACTIVE);
el.find(`${conf.parentTrigger}.${ClassName.ACTIVE}`)
.parents(conf.parentTrigger)
.children(conf.triggerElement)
.attr('aria-expanded', 'true'); // add attribute aria-expanded=true the triggers of all parents
el.find(`${conf.parentTrigger}.${ClassName.ACTIVE}`)
.has(conf.subMenu)
.children(conf.subMenu)
.addClass(`${ClassName.COLLAPSE} ${ClassName.SHOW}`);
el
.find(conf.parentTrigger)
.not(`.${ClassName.ACTIVE}`)
.has(conf.subMenu)
.children(conf.subMenu)
.addClass(ClassName.COLLAPSE);
el
.find(conf.parentTrigger)
// .has(conf.subMenu)
.children(conf.triggerElement)
.on(Event.CLICK_DATA_API, function (e) { // eslint-disable-line func-names
const eTar = $__default['default'](this);
if (eTar.attr('aria-disabled') === 'true') {
return;
}
if (conf.preventDefault && eTar.attr('href') === '#') {
e.preventDefault();
}
const paRent = eTar.parent(conf.parentTrigger);
const sibLi = paRent.siblings(conf.parentTrigger);
const sibTrigger = sibLi.children(conf.triggerElement);
if (paRent.hasClass(ClassName.ACTIVE)) {
eTar.attr('aria-expanded', 'false');
self.removeActive(paRent);
} else {
eTar.attr('aria-expanded', 'true');
self.setActive(paRent);
if (conf.toggle) {
self.removeActive(sibLi);
sibTrigger.attr('aria-expanded', 'false');
}
}
if (conf.onTransitionStart) {
conf.onTransitionStart(e);
}
});
}
setActive(li) {
$__default['default'](li).addClass(ClassName.ACTIVE);
const ul = $__default['default'](li).children(this.config.subMenu);
if (ul.length > 0 && !ul.hasClass(ClassName.SHOW)) {
this.show(ul);
}
}
removeActive(li) {
$__default['default'](li).removeClass(ClassName.ACTIVE);
const ul = $__default['default'](li).children(`${this.config.subMenu}.${ClassName.SHOW}`);
if (ul.length > 0) {
this.hide(ul);
}
}
show(element) {
if (this.transitioning || $__default['default'](element).hasClass(ClassName.COLLAPSING)) {
return;
}
const elem = $__default['default'](element);
const startEvent = $__default['default'].Event(Event.SHOW);
elem.trigger(startEvent);
if (startEvent.isDefaultPrevented()) {
return;
}
elem.parent(this.config.parentTrigger).addClass(ClassName.ACTIVE);
if (this.config.toggle) {
const toggleElem = elem.parent(this.config.parentTrigger).siblings().children(`${this.config.subMenu}.${ClassName.SHOW}`);
this.hide(toggleElem);
}
elem
.removeClass(ClassName.COLLAPSE)
.addClass(ClassName.COLLAPSING)
.height(0);
this.setTransitioning(true);
const complete = () => {
// check if disposed
if (!this.config || !this.element) {
return;
}
elem
.removeClass(ClassName.COLLAPSING)
.addClass(`${ClassName.COLLAPSE} ${ClassName.SHOW}`)
.height('');
this.setTransitioning(false);
elem.trigger(Event.SHOWN);
};
elem
.height(element[0].scrollHeight)
.one(Util.TRANSITION_END, complete)
.mmEmulateTransitionEnd(TRANSITION_DURATION);
}
hide(element) {
if (
this.transitioning || !$__default['default'](element).hasClass(ClassName.SHOW)
) {
return;
}
const elem = $__default['default'](element);
const startEvent = $__default['default'].Event(Event.HIDE);
elem.trigger(startEvent);
if (startEvent.isDefaultPrevented()) {
return;
}
elem.parent(this.config.parentTrigger).removeClass(ClassName.ACTIVE);
// eslint-disable-next-line no-unused-expressions
elem.height(elem.height())[0].offsetHeight;
elem
.addClass(ClassName.COLLAPSING)
.removeClass(ClassName.COLLAPSE)
.removeClass(ClassName.SHOW);
this.setTransitioning(true);
const complete = () => {
// check if disposed
if (!this.config || !this.element) {
return;
}
if (this.transitioning && this.config.onTransitionEnd) {
this.config.onTransitionEnd();
}
this.setTransitioning(false);
elem.trigger(Event.HIDDEN);
elem
.removeClass(ClassName.COLLAPSING)
.addClass(ClassName.COLLAPSE);
};
if (elem.height() === 0 || elem.css('display') === 'none') {
complete();
} else {
elem
.height(0)
.one(Util.TRANSITION_END, complete)
.mmEmulateTransitionEnd(TRANSITION_DURATION);
}
}
setTransitioning(isTransitioning) {
this.transitioning = isTransitioning;
}
dispose() {
$__default['default'].removeData(this.element, DATA_KEY);
$__default['default'](this.element)
.find(this.config.parentTrigger)
// .has(this.config.subMenu)
.children(this.config.triggerElement)
.off(Event.CLICK_DATA_API);
this.transitioning = null;
this.config = null;
this.element = null;
}
static jQueryInterface(config) {
// eslint-disable-next-line func-names
return this.each(function () {
const $this = $__default['default'](this);
let data = $this.data(DATA_KEY);
const conf = {
...Default,
...$this.data(),
...(typeof config === 'object' && config ? config : {}),
};
if (!data) {
data = new MetisMenu(this, conf);
$this.data(DATA_KEY, data);
}
if (typeof config === 'string') {
if (data[config] === undefined) {
throw new Error(`No method named "${config}"`);
}
data[config]();
}
});
}
}
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
*/
$__default['default'].fn[NAME] = MetisMenu.jQueryInterface; // eslint-disable-line no-param-reassign
$__default['default'].fn[NAME].Constructor = MetisMenu; // eslint-disable-line no-param-reassign
$__default['default'].fn[NAME].noConflict = () => {
// eslint-disable-line no-param-reassign
$__default['default'].fn[NAME] = JQUERY_NO_CONFLICT; // eslint-disable-line no-param-reassign
return MetisMenu.jQueryInterface;
};
return MetisMenu;
})));
//# sourceMappingURL=metisMenu.js.map

View File

@@ -1,120 +0,0 @@
/*
* metismenu - v1.1.3
* Easy menu jQuery plugin for Twitter Bootstrap 3
* https://github.com/onokumus/metisMenu
*
* Made by Osman Nuri Okumus
* Under MIT License
*/
;(function($, window, document, undefined) {
var pluginName = "metisMenu",
defaults = {
toggle: true,
doubleTapToGo: false
};
function Plugin(element, options) {
this.element = $(element);
this.settings = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
Plugin.prototype = {
init: function() {
var $this = this.element,
$toggle = this.settings.toggle,
obj = this;
if (this.isIE() <= 9) {
$this.find("li.active").has("ul").children("ul").collapse("show");
$this.find("li").not(".active").has("ul").children("ul").collapse("hide");
} else {
$this.find("li.active").has("ul").children("ul").addClass("collapse in");
$this.find("li").not(".active").has("ul").children("ul").addClass("collapse");
}
//add the "doubleTapToGo" class to active items if needed
if (obj.settings.doubleTapToGo) {
$this.find("li.active").has("ul").children("a").addClass("doubleTapToGo");
}
$this.find("li").has("ul").children("a").on("click" + "." + pluginName, function(e) {
e.preventDefault();
//Do we need to enable the double tap
if (obj.settings.doubleTapToGo) {
//if we hit a second time on the link and the href is valid, navigate to that url
if (obj.doubleTapToGo($(this)) && $(this).attr("href") !== "#" && $(this).attr("href") !== "") {
e.stopPropagation();
document.location = $(this).attr("href");
return;
}
}
$(this).parent("li").toggleClass("active").children("ul").collapse("toggle");
if ($toggle) {
$(this).parent("li").siblings().removeClass("active").children("ul.in").collapse("hide");
}
});
},
isIE: function() { //https://gist.github.com/padolsey/527683
var undef,
v = 3,
div = document.createElement("div"),
all = div.getElementsByTagName("i");
while (
div.innerHTML = "<!--[if gt IE " + (++v) + "]><i></i><![endif]-->",
all[0]
) {
return v > 4 ? v : undef;
}
},
//Enable the link on the second click.
doubleTapToGo: function(elem) {
var $this = this.element;
//if the class "doubleTapToGo" exists, remove it and return
if (elem.hasClass("doubleTapToGo")) {
elem.removeClass("doubleTapToGo");
return true;
}
//does not exists, add a new class and return false
if (elem.parent().children("ul").length) {
//first remove all other class
$this.find(".doubleTapToGo").removeClass("doubleTapToGo");
//add the class on the current element
elem.addClass("doubleTapToGo");
return false;
}
},
remove: function() {
this.element.off("." + pluginName);
this.element.removeData(pluginName);
}
};
$.fn[pluginName] = function(options) {
this.each(function () {
var el = $(this);
if (el.data(pluginName)) {
el.data(pluginName).remove();
}
el.data(pluginName, new Plugin(this, options));
});
return this;
};
})(jQuery, window, document);

View File

@@ -1,7 +1,7 @@
{% load i18n %}
{% load static %}
<!-- Mainly scripts -->
<script src="{% static "js/plugins/metisMenu/jquery.metisMenu.js" %}"></script>
<script src="{% static "js/plugins/metisMenu/jquery.metisMenu.3.0.7.js" %}"></script>
<!-- Custom and plugin javascript -->
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
@@ -14,7 +14,7 @@
{% if INTERFACE.footer_content %}
<style>
.markdown-footer a {
color: inherit;
color: #428bca;
}
.markdown-footer {
@@ -39,12 +39,23 @@
if ($('.tooltip')[0]) {
$('.tooltip').tooltip();
}
$.fn.select2.defaults.set('language', getUserLang())
const md = window.markdownit();
const markdownContent = document.querySelector('script[type="text/markdown"]').textContent;
const markdownRef = document.getElementById('markdown-output')
if (markdownRef) {
markdownRef.innerHTML = md.render(markdownContent);
$.fn.select2.defaults.set('language', getUserLang());
const md = window.markdownit({
html: true,
linkify: true,
typographer: true,
breaks: true
});
const markdownContent = `{{ INTERFACE.footer_content|escapejs }}`;
const markdownRef = document.getElementById('markdown-output');
if (markdownRef && markdownContent) {
const renderedContent = md.render(markdownContent.trim());
markdownRef.innerHTML = renderedContent;
markdownRef.querySelectorAll('a').forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
});
}
});
</script>

View File

@@ -0,0 +1,44 @@
{% extends '_base_only_content.html' %}
{% load static %}
{% load i18n %}
{% block html_title %} {{ INTERFACE.login_title }} {% endblock %}
{% block title %} {{ INTERFACE.login_title }}{% endblock %}
{% block content %}
<style>
.alert.alert-msg {
background: #F5F5F7;
}
.target-url {
display: inline-block;
max-width: 100%;
min-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
</style>
<div>
<p>
<div class="alert {% if error %} alert-danger {% else %} alert-info {% endif %}" id="messages">
{% trans 'You are about to be redirected to an external website. Please confirm that you trust this link: ' %}
<a class="target-url" href="{{ target_url }}">{{ target_url }}</a>
</div>
</p>
<div class="row">
<div class="col-sm-3">
<a href="/" class="btn btn-default block full-width m-b">
{% trans 'Cancel' %}
</a>
</div>
<div class="col-sm-3">
<a href="{{ target_url }}" class="btn btn-primary block full-width m-b">
{% trans 'Confirm' %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,24 +1,26 @@
# -*- coding: utf-8 -*-
#
import logging
from django.db.models import Q
from django.conf import settings
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as filters
from rest_framework import generics
from rest_framework import status
from rest_framework.views import APIView, Response
from django_filters import rest_framework as filters
from common.drf.filters import BaseFilterSet
from common.api import JMSBulkModelViewSet
from common.drf.filters import BaseFilterSet
from common.exceptions import JMSException
from common.permissions import WithBootstrapToken
from common.permissions import WithBootstrapToken, IsServiceAccount
from jumpserver.conf import ConfigCrypto
from terminal import serializers
from terminal.models import Terminal
__all__ = [
'TerminalViewSet', 'TerminalConfig',
'TerminalRegistrationApi',
'TerminalRegistrationApi', 'EncryptedTerminalConfig'
]
logger = logging.getLogger(__file__)
@@ -89,3 +91,17 @@ class TerminalRegistrationApi(generics.CreateAPIView):
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return super().create(request, *args, **kwargs)
class EncryptedTerminalConfig(generics.CreateAPIView):
serializer_class = serializers.EncryptedConfigSerializer
permission_classes = [IsServiceAccount]
http_method_names = ['post']
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
encrypt_key = serializer.validated_data['secret_encrypt_key']
encrypted_value = serializer.validated_data['encrypted_value']
config_crypto = ConfigCrypto(encrypt_key)
value = config_crypto.decrypt(encrypted_value)
return Response(data={'value': value}, status=200)

View File

@@ -4,6 +4,7 @@ import os
import tarfile
from django.core.files.storage import default_storage
from django.conf import settings
from django.db.models import F
from django.http import FileResponse
from django.shortcuts import get_object_or_404, reverse
@@ -156,6 +157,8 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet):
@action(methods=[GET], detail=False, permission_classes=[IsAuthenticated], url_path='online-info', )
def online_info(self, request, *args, **kwargs):
if not settings.VIEW_ASSET_ONLINE_SESSION_INFO:
return self.permission_denied(request, "view asset online session info disabled")
asset = self.request.query_params.get('asset_id')
account = self.request.query_params.get('account')
if asset is None or account is None:
@@ -189,15 +192,16 @@ class SessionViewSet(RecordViewLogMixin, OrgBulkModelViewSet):
return Response({'msg': 'ok', 'id': activity_log.id})
def get_queryset(self):
queryset = super().get_queryset() \
.prefetch_related('terminal') \
.annotate(terminal_display=F('terminal__name'))
return queryset
queryset = super().get_queryset()
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# 解决guacamole更新session时并发导致幽灵会话的问题暂不处理
if self.request.method in ('PATCH',):
if self.request.method in ('GET',):
queryset = (
queryset.prefetch_related('terminal')
.annotate(terminal_display=F('terminal__name'))
)
elif self.request.method in ('PATCH',):
# postgres reports an error for statements that use select_for_update for out join
# so we need to use select_for_update only for have not prefetch_related and annotate
queryset = queryset.select_for_update()
return queryset

View File

@@ -105,6 +105,8 @@ class AppletMethod:
if not has_applet_hosts:
return methods
applets = Applet.objects.filter(is_active=True)
if not settings.XPACK_LICENSE_IS_VALID:
applets = applets.filter(builtin=True)
for applet in applets:
for protocol in applet.protocols:
methods[protocol].append({
@@ -125,6 +127,8 @@ class VirtualAppMethod:
methods = defaultdict(list)
if not getattr(settings, 'VIRTUAL_APP_ENABLED'):
return methods
if not settings.XPACK_LICENSE_IS_VALID:
return methods
virtual_apps = VirtualApp.objects.filter(is_active=True)
for virtual_app in virtual_apps:
for protocol in virtual_app.protocols:

View File

@@ -179,7 +179,6 @@ class Applet(JMSBaseModel):
if host_matched:
return random.choice(host_matched)
hosts = [h for h in hosts if h.auto_create_accounts]
prefer_key = self.host_prefer_key_tpl.format(user.id)
prefer_host_id = cache.get(prefer_key, None)
pref_host = [host for host in hosts if host.id == prefer_host_id]

View File

@@ -5,7 +5,7 @@ from django.core.cache import cache
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.const.signals import SKIP_SIGNAL
from common.const.signals import OP_LOG_SKIP_SIGNAL
from common.db.models import JMSBaseModel
from common.utils import get_logger, lazyproperty
from orgs.utils import tmp_to_root_org
@@ -152,7 +152,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
def delete(self, using=None, keep_parents=False):
if self.user:
setattr(self.user, SKIP_SIGNAL, True)
setattr(self.user, OP_LOG_SKIP_SIGNAL, True)
self.user.delete()
self.name = self.name + '_' + uuid.uuid4().hex[:8]
self.user = None

View File

@@ -12,6 +12,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from assets.models import Asset
from common.const import OP_LOG_SKIP_SIGNAL
from common.utils import get_object_or_none, lazyproperty
from orgs.mixins.models import OrgModelMixin
from terminal.backends import get_multi_command_storage
@@ -203,6 +204,7 @@ class Session(OrgModelMixin):
if self.need_update_cmd_amount:
cmd_amount = self.compute_command_amount()
self.cmd_amount = cmd_amount
setattr(self, OP_LOG_SKIP_SIGNAL, True)
self.save()
elif self.need_compute_cmd_amount:
cmd_amount = self.compute_command_amount()

View File

@@ -9,7 +9,13 @@ __all__ = ['IsSessionAssignee']
class IsSessionAssignee(permissions.IsAuthenticated):
def has_permission(self, request, view):
return True
if not request.user:
return False
if request.user.is_anonymous:
return False
if view.action == 'retrieve':
return True
return False
def has_object_permission(self, request, view, obj):
try:

View File

@@ -1,7 +1,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
from acls.serializers.rules import address_validator, ip_group_help_text
from common.serializers import BulkModelSerializer
from common.serializers.fields import ObjectRelatedField
from ..models import Endpoint, EndpointRule
@@ -61,13 +61,13 @@ class EndpointSerializer(BulkModelSerializer):
class EndpointRuleSerializer(BulkModelSerializer):
_ip_group_help_text = '{}, {} <br>{}'.format(
_('The assets within this IP range, the following endpoint will be used for the connection'),
_('The assets within this IP range or Host, the following endpoint will be used for the connection'),
_('If asset IP addresses under different endpoints conflict, use asset labels'),
ip_group_help_text,
)
ip_group = serializers.ListField(
default=['*'], label=_('Asset IP'), help_text=_ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
default=['*'], label=_('Address'), help_text=_ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[address_validator]),
)
endpoint = ObjectRelatedField(
allow_null=True, required=False, queryset=Endpoint.objects, label=_('Endpoint')

View File

@@ -147,3 +147,8 @@ class ConnectMethodSerializer(serializers.Serializer):
type = serializers.CharField(max_length=128)
endpoint_protocol = serializers.CharField(max_length=128)
component = serializers.CharField(max_length=128)
class EncryptedConfigSerializer(serializers.Serializer):
secret_encrypt_key = serializers.CharField(max_length=128)
encrypted_value = serializers.CharField(max_length=128)

View File

@@ -54,6 +54,7 @@ urlpatterns = [
# components
path('components/metrics/', api.ComponentsMetricsAPIView.as_view(), name='components-metrics'),
path('components/connect-methods/', api.ConnectMethodListApi.as_view(), name='connect-methods'),
path('encrypted-config/', api.EncryptedTerminalConfig.as_view(), name='encrypted-terminal-config'),
]
urlpatterns += router.urls

View File

@@ -97,10 +97,10 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
def convert_status_metrics(metrics):
return {
'any': metrics['total'],
'normal': metrics['normal'],
'high': metrics['high'],
'critical': metrics['critical'],
'offline': metrics['offline']
'normal': len(metrics['normal']),
'high': len(metrics['high']),
'critical': len(metrics['critical']),
'offline': len(metrics['offline'])
}
def get_component_status_metrics(self):
@@ -112,8 +112,8 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil):
tp = metric['type']
prometheus_metrics.append(f'## 组件: {tp}')
status_metrics = self.convert_status_metrics(metric)
for status, value in status_metrics.items():
metric_text = status_metric_text % (tp, status, value)
for status, count in status_metrics.items():
metric_text = status_metric_text % (tp, status, count)
prometheus_metrics.append(metric_text)
return prometheus_metrics

View File

@@ -23,7 +23,8 @@ class TicketFilter(BaseFilterSet):
def filter_assignees_id(self, queryset, name, value):
return queryset.filter(
ticket_steps__ticket_assignees__assignee__id=value
ticket_steps__level=F('approval_step'),
ticket_steps__ticket_assignees__assignee_id=value
)
def filter_relevant_asset(self, queryset, name, value):

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