diff --git a/Dockerfile b/Dockerfile index da9ff59d2..82f87db5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ ENV LANG=en_US.UTF-8 \ PATH=/opt/py3/bin:$PATH ARG DEPENDENCIES=" \ + libldap2-dev \ libx11-dev" ARG TOOLS=" \ diff --git a/apps/accounts/automations/change_secret/host/windows_rdp_verify/main.yml b/apps/accounts/automations/change_secret/host/windows_rdp_verify/main.yml index 31da190ef..6c59ef7ef 100644 --- a/apps/accounts/automations/change_secret/host/windows_rdp_verify/main.yml +++ b/apps/accounts/automations/change_secret/host/windows_rdp_verify/main.yml @@ -30,6 +30,6 @@ login_user: "{{ account.username }}" login_password: "{{ account.secret }}" login_secret_type: "{{ account.secret_type }}" - gateway_args: "{{ jms_gateway | default(None) }}" + gateway_args: "{{ jms_gateway | default({}) }}" when: account.secret_type == "password" delegate_to: localhost diff --git a/apps/accounts/automations/push_account/host/windows_rdp_verify/main.yml b/apps/accounts/automations/push_account/host/windows_rdp_verify/main.yml index e15b5889e..0c650bfb3 100644 --- a/apps/accounts/automations/push_account/host/windows_rdp_verify/main.yml +++ b/apps/accounts/automations/push_account/host/windows_rdp_verify/main.yml @@ -30,6 +30,6 @@ login_user: "{{ account.username }}" login_password: "{{ account.secret }}" login_secret_type: "{{ account.secret_type }}" - gateway_args: "{{ jms_gateway | default(None) }}" + gateway_args: "{{ jms_gateway | default({}) }}" when: account.secret_type == "password" delegate_to: localhost diff --git a/apps/accounts/backends/__init__.py b/apps/accounts/backends/__init__.py index 832033b73..5b880da68 100644 --- a/apps/accounts/backends/__init__.py +++ b/apps/accounts/backends/__init__.py @@ -1,18 +1,21 @@ from importlib import import_module -from django.utils.functional import LazyObject +from django.utils.functional import LazyObject, empty from common.utils import get_logger from ..const import VaultTypeChoices -__all__ = ['vault_client', 'get_vault_client'] - +__all__ = ['vault_client', 'get_vault_client', 'refresh_vault_client'] logger = get_logger(__file__) def get_vault_client(raise_exception=False, **kwargs): tp = kwargs.get('VAULT_BACKEND') if kwargs.get('VAULT_ENABLED') else VaultTypeChoices.local + + # TODO: Temporary processing, subsequent deletion + tp = VaultTypeChoices.local if tp == VaultTypeChoices.azure else tp + try: module_path = f'apps.accounts.backends.{tp}.main' client = import_module(module_path).Vault(**kwargs) @@ -38,3 +41,7 @@ class VaultClient(LazyObject): """ 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """ vault_client = VaultClient() + + +def refresh_vault_client(): + vault_client._wrapped = empty diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py index c0419486d..d8388b6bc 100644 --- a/apps/accounts/const/automation.py +++ b/apps/accounts/const/automation.py @@ -49,9 +49,9 @@ class SecretStrategy(models.TextChoices): class SSHKeyStrategy(models.TextChoices): - add = 'add', _('Append SSH KEY') - set = 'set', _('Empty and append SSH KEY') set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ') + set = 'set', _('Empty and append SSH KEY') + add = 'add', _('Append SSH KEY') class TriggerChoice(models.TextChoices, TreeChoices): diff --git a/apps/accounts/migrations/0002_auto_20220616_0021.py b/apps/accounts/migrations/0002_auto_20220616_0021.py index 5ac195d63..8fe829dd6 100644 --- a/apps/accounts/migrations/0002_auto_20220616_0021.py +++ b/apps/accounts/migrations/0002_auto_20220616_0021.py @@ -50,7 +50,7 @@ class Migration(migrations.Migration): ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')), ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), - ('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')), + ('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')), ], options={ 'verbose_name': 'Change secret automation', @@ -76,7 +76,7 @@ class Migration(migrations.Migration): ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')), ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), - ('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')), + ('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')), ('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')), ('username', models.CharField(max_length=128, verbose_name='Username')), ('action', models.CharField(max_length=16, verbose_name='Action')), diff --git a/apps/accounts/models/automations/base.py b/apps/accounts/models/automations/base.py index 1adc84bfe..b001e8cc7 100644 --- a/apps/accounts/models/automations/base.py +++ b/apps/accounts/models/automations/base.py @@ -51,7 +51,7 @@ class AutomationExecution(AssetAutomationExecution): class ChangeSecretMixin(SecretWithRandomMixin): ssh_key_change_strategy = models.CharField( choices=SSHKeyStrategy.choices, max_length=16, - default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy') + default=SSHKeyStrategy.set_jms, verbose_name=_('SSH key change strategy') ) get_all_assets: callable # get all assets diff --git a/apps/accounts/signal_handlers.py b/apps/accounts/signal_handlers.py index ad07c9bb3..04c2cc333 100644 --- a/apps/accounts/signal_handlers.py +++ b/apps/accounts/signal_handlers.py @@ -3,14 +3,17 @@ from collections import defaultdict from django.db.models.signals import post_delete from django.db.models.signals import pre_save, post_save from django.dispatch import receiver +from django.utils.functional import LazyObject from django.utils.translation import gettext_noop -from accounts.backends import vault_client +from accounts.backends import vault_client, refresh_vault_client from accounts.const import Source from audits.const import ActivityChoices from audits.signal_handlers import create_activities from common.decorators import merge_delay_run +from common.signals import django_ready from common.utils import get_logger, i18n_fmt +from common.utils.connection import RedisPubSub from .models import Account, AccountTemplate from .tasks.push_account import push_accounts_to_assets_task @@ -91,3 +94,18 @@ class VaultSignalHandler(object): for model in (Account, AccountTemplate, Account.history.model): post_save.connect(VaultSignalHandler.save_to_vault, sender=model) post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model) + + +class VaultPubSub(LazyObject): + def _setup(self): + self._wrapped = RedisPubSub('refresh_vault') + + +vault_pub_sub = VaultPubSub() + + +@receiver(django_ready) +def subscribe_vault_change(sender, **kwargs): + logger.debug("Start subscribe vault change") + + vault_pub_sub.subscribe(lambda name: refresh_vault_client()) diff --git a/apps/audits/api.py b/apps/audits/api.py index d586f8e3b..0c7278476 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -7,6 +7,7 @@ from django.db.models import F, Value, CharField, Q from django.db.models.functions import Cast from django.http import HttpResponse, FileResponse from django.utils.encoding import escape_uri_path +from django_celery_beat.models import PeriodicTask from rest_framework import generics from rest_framework import status from rest_framework import viewsets @@ -74,6 +75,21 @@ class JobsAuditViewSet(OrgModelViewSet): queryset = queryset.exclude(type=Types.upload_file).filter(instant=False) return queryset + def perform_update(self, serializer): + job = self.get_object() + is_periodic = serializer.validated_data.get('is_periodic') + if job.is_periodic != is_periodic: + job.is_periodic = is_periodic + job.save() + name, task, args, kwargs = job.get_register_task() + task_obj = PeriodicTask.objects.filter(name=name).first() + if task_obj: + is_periodic = job.is_periodic + if task_obj.enabled != is_periodic: + task_obj.enabled = is_periodic + task_obj.save() + return super().perform_update(serializer) + class FTPLogViewSet(OrgModelViewSet): model = FTPLog diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index b9d41f8e5..48c408fe2 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -38,10 +38,12 @@ class JobsAuditSerializer(JobSerializer): material = serializers.ReadOnlyField(label=_("Command")) summary = serializers.ReadOnlyField(label=_("Summary")) crontab = serializers.ReadOnlyField(label=_("Execution cycle")) + is_periodic_display = serializers.BooleanField(read_only=True, source='is_periodic') class Meta(JobSerializer.Meta): read_only_fields = [ - "id", 'name', 'args', 'material', 'type', 'crontab', 'interval', 'date_last_run', 'summary', 'created_by' + "id", 'name', 'args', 'material', 'type', 'crontab', 'interval', 'date_last_run', 'summary', 'created_by', + 'is_periodic_display' ] fields = read_only_fields + ['is_periodic'] diff --git a/apps/authentication/mfa/face.py b/apps/authentication/mfa/face.py index c37eae526..60ff2c247 100644 --- a/apps/authentication/mfa/face.py +++ b/apps/authentication/mfa/face.py @@ -5,6 +5,7 @@ from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ from authentication.mixins import MFAFaceMixin +from common.const import LicenseEditionChoices from settings.api import settings @@ -32,7 +33,10 @@ class MFAFace(BaseMFA, MFAFaceMixin): @staticmethod def global_enabled(): - return settings.XPACK_LICENSE_IS_VALID and settings.FACE_RECOGNITION_ENABLED + return settings.XPACK_LICENSE_IS_VALID \ + and LicenseEditionChoices.ULTIMATE == \ + LicenseEditionChoices.from_key(settings.XPACK_LICENSE_EDITION) \ + and settings.FACE_RECOGNITION_ENABLED def get_enable_url(self) -> str: return reverse('authentication:user-face-enable') diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py index d1f3b087b..64f8818a9 100644 --- a/apps/authentication/middleware.py +++ b/apps/authentication/middleware.py @@ -35,7 +35,7 @@ class MFAMiddleware: # 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了 white_urls = [ - 'login/mfa', 'mfa/select', 'jsi18n/', '/static/', + 'login/mfa', 'mfa/select', 'mfa/face','jsi18n/', '/static/', '/profile/otp', '/logout/', ] for url in white_urls: diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index c00335ec0..92b49f64e 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -238,7 +238,7 @@ class MFAFaceMixin: if not self.is_context_success(context): msg = context.get('error_message', '') - raise RuntimeError("Face recognition failed,{}".format(msg)) + raise RuntimeError(msg) face_code = context.get('face_code') if not face_code: diff --git a/apps/authentication/templates/authentication/face_capture.html b/apps/authentication/templates/authentication/face_capture.html index 4d0f66096..f04716a59 100644 --- a/apps/authentication/templates/authentication/face_capture.html +++ b/apps/authentication/templates/authentication/face_capture.html @@ -2,28 +2,18 @@ {% load i18n %} {% load static %} - {% block content %} - - {% if 'code' in form.errors %} -
{{ form.code.errors.as_text }}
+{{ form.code.errors.as_text }}
+