mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-24 21:12:35 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83eeeb568 | ||
|
|
7ab5b3efab | ||
|
|
81254be293 | ||
|
|
d5df73e1ad | ||
|
|
a497b3cf94 | ||
|
|
8548b73063 | ||
|
|
182320f492 | ||
|
|
40d326d6a6 | ||
|
|
b87554f9db | ||
|
|
3c255f9fa6 | ||
|
|
a6b5437f6a | ||
|
|
71c690ef9e | ||
|
|
bee07db900 | ||
|
|
115eb7c15a | ||
|
|
88810263cd | ||
|
|
3d95bc4656 | ||
|
|
69de08bb5d | ||
|
|
5c234fdd0c | ||
|
|
be5baa5a3f | ||
|
|
2f1a65f120 | ||
|
|
e6d02eaf4c | ||
|
|
6d6dec2752 | ||
|
|
c6c067c44b | ||
|
|
84ec1b047a | ||
|
|
e6dca2ec14 | ||
|
|
8793003d18 | ||
|
|
29fd6e0ae4 | ||
|
|
90587a83cc | ||
|
|
dfa0198742 | ||
|
|
9e857b54ed | ||
|
|
c34358509b | ||
|
|
a7c46109d9 | ||
|
|
48fa6172bd | ||
|
|
89aa87fd6b | ||
|
|
79d230755e | ||
|
|
99082f261e | ||
|
|
7e2100b435 | ||
|
|
185d4e9563 | ||
|
|
ecaa84790c | ||
|
|
30210dc0b9 | ||
|
|
ff699f4ee2 | ||
|
|
48239b0c63 | ||
|
|
f4f74909a8 | ||
|
|
cab1e0bf52 | ||
|
|
bf195c1599 | ||
|
|
7f5f7e81b8 | ||
|
|
99affad9b9 | ||
|
|
34eea024f8 | ||
|
|
1d1e4b90ed | ||
|
|
f5d40a787e | ||
|
|
77d8083c00 | ||
|
|
180303ccb4 | ||
|
|
9cd1619990 | ||
|
|
7d0a901522 | ||
|
|
5e9fabff1b | ||
|
|
1d36934111 | ||
|
|
25603e4758 | ||
|
|
3ae164d7e0 | ||
|
|
3ad64e142e | ||
|
|
0ff1413780 | ||
|
|
f5b64bed4e | ||
|
|
a559415b65 | ||
|
|
2e7bd076f4 | ||
|
|
11f6fe0bf9 | ||
|
|
ae94648e80 | ||
|
|
94e08f3d96 | ||
|
|
8bedef92f0 | ||
|
|
e5bb28231a | ||
|
|
b5aeb24ae9 | ||
|
|
674ea7142f | ||
|
|
5ab7b99b9d | ||
|
|
9cd163c99d | ||
|
|
e72073f0cc | ||
|
|
690f525afc |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_timeout: 30
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: '
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_timeout: 30
|
||||
tasks:
|
||||
- name: Posix ping
|
||||
ansible.builtin.ping:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
- hosts: windows
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_timeout: 30
|
||||
tasks:
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
cipher_alg_id = {
|
||||
"sm4_ebc": 0x00000401,
|
||||
"sm4_cbc": 0x00000402,
|
||||
"sm4_mac": 0x00000405,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
30
apps/orgs/mixins/ws.py
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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', '*', '*'),
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
263
apps/static/js/plugins/jsencrypt/jsencrypt.3.3.2.min.js
vendored
Normal file
263
apps/static/js/plugins/jsencrypt/jsencrypt.3.3.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
368
apps/static/js/plugins/metisMenu/jquery.metisMenu.3.0.7.js
Normal file
368
apps/static/js/plugins/metisMenu/jquery.metisMenu.3.0.7.js
Normal 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
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
44
apps/templates/redirect_confirm.html
Normal file
44
apps/templates/redirect_confirm.html
Normal 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 %}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user