mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-25 21:42:37 +00:00
Compare commits
4 Commits
v4.10.14
...
pr@v5@perf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b1d61902 | ||
|
|
916dc5578a | ||
|
|
f362163af1 | ||
|
|
5f1ba56e56 |
@@ -1,4 +1,4 @@
|
||||
FROM jumpserver/core-base:20251128_025056 AS stage-build
|
||||
FROM jumpserver/core-base:20251225_092635 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -167,7 +166,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
|
||||
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSecretSerializer,
|
||||
|
||||
@@ -81,7 +81,4 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
|
||||
remote_addr=get_request_ip(request), service=service.name, service_id=service.id,
|
||||
account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})',
|
||||
)
|
||||
|
||||
# 根据配置决定是否返回密码
|
||||
secret = account.secret if settings.SECURITY_ACCOUNT_SECRET_READ else None
|
||||
return Response(data={'id': request.user.id, 'secret': secret})
|
||||
return Response(data={'id': request.user.id, 'secret': account.secret})
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
|
||||
@@ -104,7 +104,7 @@ class AutomationExecutionViewSet(
|
||||
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
search_fields = ('id', 'trigger', 'automation__name')
|
||||
search_fields = ('trigger', 'automation__name')
|
||||
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||
filterset_class = AutomationExecutionFilterSet
|
||||
serializer_class = serializers.AutomationExecutionSerializer
|
||||
|
||||
@@ -234,7 +234,7 @@ class AutomationExecutionFilterSet(DaysExecutionFilterMixin, BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = AutomationExecution
|
||||
fields = ["id", "days", 'trigger', 'automation__name']
|
||||
fields = ["days", 'trigger', 'automation__name']
|
||||
|
||||
|
||||
class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet):
|
||||
|
||||
@@ -81,9 +81,7 @@ class VaultModelMixin(models.Model):
|
||||
def mark_secret_save_to_vault(self):
|
||||
self._secret = self._secret_save_to_vault_mark
|
||||
self.skip_history_when_saving = True
|
||||
# Avoid calling overridden `save()` on concrete models (e.g. AccountTemplate)
|
||||
# which may mutate `secret/_secret` again and cause post_save recursion.
|
||||
super(VaultModelMixin, self).save(update_fields=['_secret'])
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def secret_has_save_to_vault(self):
|
||||
|
||||
@@ -14,7 +14,7 @@ from accounts.models import Account, AccountTemplate, GatheredAccount
|
||||
from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin, SecretReadableCheckMixin, CommonBulkModelSerializer
|
||||
from common.serializers import SecretReadableMixin, CommonBulkModelSerializer
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from common.utils import get_logger
|
||||
from .base import BaseAccountSerializer, AuthValidateMixin
|
||||
@@ -478,7 +478,7 @@ class AssetAccountBulkSerializer(
|
||||
return results
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountSerializer):
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||
|
||||
class Meta(AccountSerializer.Meta):
|
||||
@@ -491,10 +491,9 @@ class AccountSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, Acc
|
||||
exclude_backup_fields = [
|
||||
'passphrase', 'push_now', 'params', 'spec_info'
|
||||
]
|
||||
secret_fields = ['secret']
|
||||
|
||||
|
||||
class AccountHistorySerializer(SecretReadableCheckMixin, serializers.ModelSerializer):
|
||||
class AccountHistorySerializer(serializers.ModelSerializer):
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
|
||||
secret = serializers.CharField(label=_('Secret'), read_only=True)
|
||||
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
|
||||
@@ -510,7 +509,6 @@ class AccountHistorySerializer(SecretReadableCheckMixin, serializers.ModelSerial
|
||||
'history_user': {'label': _('User')},
|
||||
'history_date': {'label': _('Date')},
|
||||
}
|
||||
secret_fields = ['secret']
|
||||
|
||||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.models import AccountTemplate
|
||||
from common.serializers import SecretReadableMixin, SecretReadableCheckMixin
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
@@ -62,11 +62,10 @@ class AccountDetailTemplateSerializer(AccountTemplateSerializer):
|
||||
fields = AccountTemplateSerializer.Meta.fields + ['spec_info']
|
||||
|
||||
|
||||
class AccountTemplateSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountDetailTemplateSerializer):
|
||||
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountDetailTemplateSerializer):
|
||||
class Meta(AccountDetailTemplateSerializer.Meta):
|
||||
fields = AccountDetailTemplateSerializer.Meta.fields
|
||||
extra_kwargs = {
|
||||
**AccountDetailTemplateSerializer.Meta.extra_kwargs,
|
||||
'secret': {'write_only': False},
|
||||
}
|
||||
secret_fields = ['secret']
|
||||
|
||||
@@ -79,7 +79,7 @@ class VaultSignalHandler(object):
|
||||
else:
|
||||
vault_client.update(instance)
|
||||
except Exception as e:
|
||||
logger.exception('Vault save failed: %s', e)
|
||||
logger.error('Vault save failed: {}'.format(e))
|
||||
raise VaultException()
|
||||
|
||||
@staticmethod
|
||||
@@ -87,7 +87,7 @@ class VaultSignalHandler(object):
|
||||
try:
|
||||
vault_client.delete(instance)
|
||||
except Exception as e:
|
||||
logger.exception('Vault delete failed: %s', e)
|
||||
logger.error('Vault delete failed: {}'.format(e))
|
||||
raise VaultException()
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.backends import vault_client
|
||||
from accounts.const import VaultTypeChoices
|
||||
from accounts.models import AccountTemplate, Account
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_root_org
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ class BaseManager:
|
||||
self.execution.save()
|
||||
|
||||
def print_summary(self):
|
||||
content = "\nSummary: \n"
|
||||
content = "\nSummery: \n"
|
||||
for k, v in self.summary.items():
|
||||
content += f"\t - {k}: {v}\n"
|
||||
content += "\t - Using: {}s\n".format(self.duration)
|
||||
|
||||
@@ -186,6 +186,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
super().__init__(*args, **kwargs)
|
||||
self._init_field_choices()
|
||||
self._extract_accounts()
|
||||
self._set_platform()
|
||||
|
||||
def _extract_accounts(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
@@ -217,6 +218,21 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
protocols_data = [{'name': p.name, 'port': p.port} for p in protocols]
|
||||
self.initial_data['protocols'] = protocols_data
|
||||
|
||||
def _set_platform(self):
|
||||
if not hasattr(self, 'initial_data'):
|
||||
return
|
||||
platform_id = self.initial_data.get('platform')
|
||||
if not platform_id:
|
||||
return
|
||||
|
||||
if isinstance(platform_id, int) or str(platform_id).isdigit() or not isinstance(platform_id, str):
|
||||
return
|
||||
|
||||
platform = Platform.objects.filter(name=platform_id).first()
|
||||
if not platform:
|
||||
return
|
||||
self.initial_data['platform'] = platform.id
|
||||
|
||||
def _init_field_choices(self):
|
||||
request = self.context.get('request')
|
||||
if not request:
|
||||
@@ -265,8 +281,10 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
|
||||
if not platform_id and self.instance:
|
||||
platform = self.instance.platform
|
||||
else:
|
||||
elif isinstance(platform_id, int):
|
||||
platform = Platform.objects.filter(id=platform_id).first()
|
||||
else:
|
||||
platform = Platform.objects.filter(name=platform_id).first()
|
||||
|
||||
if not platform:
|
||||
raise serializers.ValidationError({'platform': _("Platform not exist")})
|
||||
@@ -297,6 +315,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
||||
|
||||
def is_valid(self, raise_exception=False):
|
||||
self._set_protocols_default()
|
||||
self._set_platform()
|
||||
return super().is_valid(raise_exception=raise_exception)
|
||||
|
||||
def validate_protocols(self, protocols_data):
|
||||
|
||||
@@ -219,18 +219,8 @@ class RDPFileClientProtocolURLMixin:
|
||||
}
|
||||
})
|
||||
else:
|
||||
if connect_method_dict['type'] == 'virtual_app':
|
||||
endpoint_protocol = 'vnc'
|
||||
token_protocol = 'vnc'
|
||||
data.update({
|
||||
'protocol': 'vnc',
|
||||
})
|
||||
else:
|
||||
endpoint_protocol = connect_method_dict['endpoint_protocol']
|
||||
token_protocol = token.protocol
|
||||
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol=endpoint_protocol,
|
||||
protocol=connect_method_dict['endpoint_protocol'],
|
||||
asset=asset
|
||||
)
|
||||
data.update({
|
||||
@@ -246,7 +236,7 @@ class RDPFileClientProtocolURLMixin:
|
||||
},
|
||||
'endpoint': {
|
||||
'host': endpoint.host,
|
||||
'port': endpoint.get_port(token.asset, token_protocol),
|
||||
'port': endpoint.get_port(token.asset, token.protocol),
|
||||
}
|
||||
})
|
||||
return data
|
||||
|
||||
@@ -108,7 +108,7 @@ class SessionAuthentication(authentication.SessionAuthentication):
|
||||
user = getattr(request._request, 'user', None)
|
||||
|
||||
# Unauthenticated, CSRF validation not required
|
||||
if not user or not user.is_active or not user.is_valid:
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
@@ -12,7 +12,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_bytes, smart_bytes
|
||||
from django.utils.encoding import force_bytes
|
||||
from jwkest import JWKESTException
|
||||
from jwkest.jwk import KEYS
|
||||
from jwkest.jws import JWS
|
||||
@@ -58,7 +58,7 @@ def _get_jwks_keys(shared_key):
|
||||
# Adds the shared key (which can correspond to the client_secret) as an oct key so it can be
|
||||
# used for HMAC signatures.
|
||||
logger.debug(log_prompt.format('Add key'))
|
||||
jwks_keys.add({'key': smart_bytes(shared_key), 'kty': 'oct'})
|
||||
jwks_keys.add({'key': force_bytes(shared_key), 'kty': 'oct'})
|
||||
logger.debug(log_prompt.format('End'))
|
||||
return jwks_keys
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class MFAMiddleware:
|
||||
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
|
||||
white_urls = [
|
||||
'login/mfa', 'mfa/select', 'face/context', 'jsi18n/', '/static/',
|
||||
'/profile/otp', '/logout/', '/media/'
|
||||
'/profile/otp', '/logout/',
|
||||
]
|
||||
for url in white_urls:
|
||||
if request.path.find(url) > -1:
|
||||
|
||||
@@ -67,6 +67,7 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
mfa_context = self.get_user_mfa_context(user)
|
||||
print(mfa_context)
|
||||
kwargs.update(mfa_context)
|
||||
return kwargs
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.test import Client
|
||||
from django.urls import URLPattern, URLResolver
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
from jumpserver.urls import api_v1
|
||||
|
||||
@@ -80,8 +81,7 @@ known_unauth_urls = [
|
||||
"/api/v1/authentication/mfa/send-code/",
|
||||
"/api/v1/authentication/sso/login/",
|
||||
"/api/v1/authentication/user-session/",
|
||||
"/api/v1/settings/i18n/zh-hans/",
|
||||
"/api/v1/settings/client/versions/"
|
||||
"/api/v1/settings/i18n/zh-hans/"
|
||||
]
|
||||
|
||||
known_error_urls = [
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import os
|
||||
from ctypes import *
|
||||
|
||||
from .exception import PiicoError
|
||||
from .session import Session
|
||||
from .cipher import *
|
||||
from .digest import *
|
||||
from django.core.cache import cache
|
||||
from redis_lock import Lock as RedisLock
|
||||
|
||||
|
||||
class Device:
|
||||
@@ -74,28 +71,10 @@ class Device:
|
||||
self.__device = device
|
||||
|
||||
def __reset_key_store(self):
|
||||
redis_client = cache.client.get_client()
|
||||
server_hostname = os.environ.get("SERVER_HOSTNAME")
|
||||
RESET_LOCK_KEY = f"spiico:{server_hostname}:reset"
|
||||
LOCK_EXPIRE_SECONDS = 300
|
||||
|
||||
if self._driver is None:
|
||||
raise PiicoError("no driver loaded", 0)
|
||||
if self.__device is None:
|
||||
raise PiicoError("device not open", 0)
|
||||
|
||||
# ---- 分布式锁(Redis-Lock 实现 Redlock) ----
|
||||
lock = RedisLock(
|
||||
redis_client,
|
||||
RESET_LOCK_KEY,
|
||||
expire=LOCK_EXPIRE_SECONDS, # 锁自动过期
|
||||
auto_renewal=False, # 不自动续租
|
||||
)
|
||||
|
||||
# 尝试获取锁,拿不到直接返回
|
||||
if not lock.acquire(blocking=False):
|
||||
return
|
||||
# ---- 真正执行 reset ----
|
||||
ret = self._driver.SPII_ResetModule(self.__device)
|
||||
if ret != 0:
|
||||
raise PiicoError("reset device failed", ret)
|
||||
raise PiicoError("reset device failed", ret)
|
||||
|
||||
@@ -30,30 +30,12 @@ __all__ = [
|
||||
"CommonSerializerMixin",
|
||||
"CommonBulkSerializerMixin",
|
||||
"SecretReadableMixin",
|
||||
"SecretReadableCheckMixin",
|
||||
"CommonModelSerializer",
|
||||
"CommonBulkModelSerializer",
|
||||
"ResourceLabelsMixin",
|
||||
]
|
||||
|
||||
|
||||
class SecretReadableCheckMixin(serializers.Serializer):
|
||||
"""
|
||||
根据 SECURITY_ACCOUNT_SECRET_READ 配置控制密码字段的可读性
|
||||
当配置为 False 时,密码字段返回 None
|
||||
"""
|
||||
|
||||
def to_representation(self, instance):
|
||||
ret = super().to_representation(instance)
|
||||
|
||||
if not settings.SECURITY_ACCOUNT_SECRET_READ:
|
||||
secret_fields = getattr(self.Meta, 'secret_fields', ['secret'])
|
||||
for field_name in secret_fields:
|
||||
if field_name in ret:
|
||||
ret[field_name] = '<REDACTED>'
|
||||
return ret
|
||||
|
||||
|
||||
class SecretReadableMixin(serializers.Serializer):
|
||||
"""加密字段 (EncryptedField) 可读性"""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1636,8 +1636,7 @@
|
||||
"setVariable": "Set variable",
|
||||
"userId": "User ID",
|
||||
"userName": "User name",
|
||||
"AccountSecretReadDisabled": "Account secret reading has been disabled by administrator",
|
||||
"AccessToken": "Access tokens",
|
||||
"AccessTokenTip": "Access Token is a temporary credential generated through the OAuth2 (Authorization Code Grant) flow using the JumpServer client, which is used to access protected resources.",
|
||||
"Revoke": "Revoke"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "Distribución de visitas",
|
||||
"AccessIP": "Lista blanca de IP",
|
||||
"AccessKey": "Clave de acceso",
|
||||
"AccessToken": "Token de acceso",
|
||||
"AccessTokenTip": "El token de acceso es un certificado temporal generado a través del cliente JumpServer utilizando el flujo de OAuth2 (autorización por código) para acceder a recursos protegidos.",
|
||||
"Account": "Información de la cuenta",
|
||||
"AccountActivities": "Actividad de la cuenta",
|
||||
"AccountAndPasswordChangeRank": "Clasificación de cambio de contraseña de cuenta",
|
||||
@@ -46,7 +44,6 @@
|
||||
"AccountPushUpdate": "Actualizar la notificación de la cuenta",
|
||||
"AccountReport": "Informe de cuentas",
|
||||
"AccountResult": "Cambio de contraseña de cuenta exitoso/fallido",
|
||||
"AccountSecretReadDisabled": "La función de lectura de nombre de usuario y contraseña ha sido desactivada por el administrador",
|
||||
"AccountSelectHelpText": "La lista de cuentas agrega el nombre de usuario de la cuenta. Tipo de contraseña.",
|
||||
"AccountSessions": "Sesión de cuenta",
|
||||
"AccountStatisticsReport": "Informe estadístico de cuentas",
|
||||
@@ -282,7 +279,6 @@
|
||||
"CACertificate": "Certificado CA",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "Tianyi Cloud",
|
||||
"CTYunPrivate": "eCloud Nube Privada",
|
||||
"CalculationResults": "Error en la expresión cron",
|
||||
"CallRecords": "Registro de llamadas",
|
||||
@@ -1180,8 +1176,6 @@
|
||||
"RetrySelected": "Reintentar selección",
|
||||
"Review": "Revisión",
|
||||
"Reviewer": "Aprobador",
|
||||
"Revoke": "Revocar",
|
||||
"Risk": "Riesgo",
|
||||
"RiskDetection": "Detección de riesgos",
|
||||
"RiskDetectionDetail": "Detalles de detección de riesgos",
|
||||
"RiskyAccount": "Cuenta de riesgo",
|
||||
@@ -1645,7 +1639,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "Esta acción reemplazará todos los protocolos y puertos, ¿continuar?",
|
||||
"pleaseSelectAssets": "Por favor, seleccione un activo.",
|
||||
"removeWarningMsg": "¿Está seguro de que desea eliminar?",
|
||||
"selectFiles": "Se ha seleccionado el archivo número {number}",
|
||||
"selectedAssets": "Activos seleccionados",
|
||||
"setVariable": "configurar parámetros",
|
||||
"userId": "ID de usuario",
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "アクセス分布",
|
||||
"AccessIP": "IP ホワイトリスト",
|
||||
"AccessKey": "アクセスキー",
|
||||
"AccessToken": "アクセス・トークン",
|
||||
"AccessTokenTip": "アクセス・トークンは、JumpServer クライアントを通じて OAuth2(Authorization Code Grant)フローを使用して生成される一時的な証明書であり、保護されたリソースへのアクセスに使用されます。",
|
||||
"Account": "アカウント情報",
|
||||
"AccountActivities": "アカウント活動",
|
||||
"AccountAmount": "アカウント数",
|
||||
@@ -48,7 +46,6 @@
|
||||
"AccountPushUpdate": "アカウント更新プッシュ",
|
||||
"AccountReport": "アカウントレポート",
|
||||
"AccountResult": "アカウントパスワード変更成功/失敗",
|
||||
"AccountSecretReadDisabled": "アカウントパスワードの読み取り機能は管理者によって無効になっています。",
|
||||
"AccountSelectHelpText": "アカウント一覧に追加されている内容は、アカウントのユーザー名",
|
||||
"AccountSessions": " アカウントセッション ",
|
||||
"AccountStatisticsReport": "アカウント統計レポート",
|
||||
@@ -286,7 +283,6 @@
|
||||
"CACertificate": "CA 証明書",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "天翼クラウド",
|
||||
"CTYunPrivate": "イークラウド・プライベートクラウド",
|
||||
"CalculationResults": "cron 式のエラー",
|
||||
"CallRecords": "つうわきろく",
|
||||
@@ -1185,8 +1181,6 @@
|
||||
"RetrySelected": "選択したものを再試行",
|
||||
"Review": "審査",
|
||||
"Reviewer": "承認者",
|
||||
"Revoke": "取り消し",
|
||||
"Risk": "リスク",
|
||||
"RiskDetection": "リスク検出",
|
||||
"RiskDetectionDetail": "リスク検出の詳細",
|
||||
"RiskyAccount": "リスクアカウント",
|
||||
@@ -1650,7 +1644,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "この操作はすべてのプロトコルとポートを上書きしますが、続行してよろしいですか?",
|
||||
"pleaseSelectAssets": "資産を選択してください",
|
||||
"removeWarningMsg": "削除してもよろしいですか",
|
||||
"selectFiles": "{number}ファイルを選択しました。",
|
||||
"selectedAssets": "選択した資産",
|
||||
"setVariable": "パラメータ設定",
|
||||
"userId": "ユーザーID",
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "방문 분포",
|
||||
"AccessIP": "IP 화이트리스트",
|
||||
"AccessKey": "액세스 키",
|
||||
"AccessToken": "접속 토큰",
|
||||
"AccessTokenTip": "접속 토큰은 JumpServer 클라이언트를 통해 OAuth2(인증 코드 인증) 프로세스를 사용하여 생성된 임시 자격 증명으로, 보호된 리소스에 접근하는 데 사용됩니다.",
|
||||
"Account": "계정",
|
||||
"AccountActivities": "계정 활동",
|
||||
"AccountAndPasswordChangeRank": "계정 비밀번호 변경 순위",
|
||||
@@ -46,7 +44,6 @@
|
||||
"AccountPushUpdate": "계정 업데이트 푸시",
|
||||
"AccountReport": "계정 보고서",
|
||||
"AccountResult": "계정 비밀번호 변경 성공/실패",
|
||||
"AccountSecretReadDisabled": "계정 비밀번호 읽기 기능은 관리자가 비활성화하였습니다.",
|
||||
"AccountSelectHelpText": "계정 목록에 추가하는 것은 계정의 사용자 이름—암호 유형입니다.",
|
||||
"AccountSessions": "계정 세션",
|
||||
"AccountStatisticsReport": "계정 통계 보고서",
|
||||
@@ -282,7 +279,6 @@
|
||||
"CACertificate": "CA 인증서",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "천윳 클라우드",
|
||||
"CTYunPrivate": "천翼 개인 클라우드",
|
||||
"CalculationResults": "cron 표현식 오류",
|
||||
"CallRecords": "호출 기록",
|
||||
@@ -1180,8 +1176,6 @@
|
||||
"RetrySelected": "선택한 항목 재시도",
|
||||
"Review": "검토",
|
||||
"Reviewer": "승인자",
|
||||
"Revoke": "취소",
|
||||
"Risk": "위험",
|
||||
"RiskDetection": "위험 감지",
|
||||
"RiskDetectionDetail": "위험 감지 상세 정보",
|
||||
"RiskyAccount": "위험 계정",
|
||||
@@ -1645,7 +1639,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "이 작업은 모든 프로토콜과 포트를 덮어씌우게 됩니다. 계속하시겠습니까?",
|
||||
"pleaseSelectAssets": "자산을 선택해 주세요",
|
||||
"removeWarningMsg": "제거할 것인지 확실합니까?",
|
||||
"selectFiles": "선택한 파일 {number}개",
|
||||
"selectedAssets": "선택한 자산",
|
||||
"setVariable": "설정 매개변수",
|
||||
"userId": "사용자 ID",
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "Distribuição de Acesso",
|
||||
"AccessIP": "Lista branca de IP",
|
||||
"AccessKey": "Chave de Acesso",
|
||||
"AccessToken": "Token de acesso",
|
||||
"AccessTokenTip": "O token de acesso é um credencial temporário gerado pelo cliente JumpServer usando o fluxo OAuth2 (autorização por código), utilizado para acessar recursos protegidos.",
|
||||
"Account": "Informações da conta",
|
||||
"AccountActivities": "Atividades da conta",
|
||||
"AccountAndPasswordChangeRank": "Alteração de Senha por Classificação",
|
||||
@@ -46,7 +44,6 @@
|
||||
"AccountPushUpdate": " Atualização de notificação de conta ",
|
||||
"AccountReport": "Relatório de Contas",
|
||||
"AccountResult": "Alteração de senha da conta bem-sucedida/falhada",
|
||||
"AccountSecretReadDisabled": "A funcionalidade de leitura de nome de usuário e senha foi desativada pelo administrador",
|
||||
"AccountSelectHelpText": "A lista de contas inclui o nome de usuário da conta",
|
||||
"AccountSessions": "Conta de sessão ",
|
||||
"AccountStatisticsReport": "Relatório de Estatísticas de Contas",
|
||||
@@ -283,7 +280,6 @@
|
||||
"CACertificate": " Certificado CA",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "Tianyi Cloud",
|
||||
"CTYunPrivate": " eCloud Nuvem Privada",
|
||||
"CalculationResults": "Erro de expressão cron",
|
||||
"CallRecords": "Registro de chamadas",
|
||||
@@ -1181,8 +1177,6 @@
|
||||
"RetrySelected": "repetir a seleção",
|
||||
"Review": "Revisar",
|
||||
"Reviewer": "Aprovador",
|
||||
"Revoke": "Revogar",
|
||||
"Risk": "Risco",
|
||||
"RiskDetection": " Detecção de risco ",
|
||||
"RiskDetectionDetail": "Detalhes da Detecção de Risco",
|
||||
"RiskyAccount": " Conta de risco ",
|
||||
@@ -1646,7 +1640,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "Esta ação substituirá todos os protocolos e portas. Deseja continuar?",
|
||||
"pleaseSelectAssets": "Por favor, selecione um ativo.",
|
||||
"removeWarningMsg": "Tem certeza de que deseja remover",
|
||||
"selectFiles": "Foram selecionados {number} arquivos",
|
||||
"selectedAssets": "Ativos selecionados",
|
||||
"setVariable": "Parâmetros de configuração",
|
||||
"userId": "ID do usuário",
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "Распределение доступа",
|
||||
"AccessIP": "Белый список IP",
|
||||
"AccessKey": "Ключ доступа",
|
||||
"AccessToken": "Токен доступа",
|
||||
"AccessTokenTip": "Токен доступа создается через клиент JumpServer с использованием процесса OAuth2 (авторизация через код) в качестве временного удостоверения для доступа к защищенным ресурсам.",
|
||||
"Account": "Информация об УЗ",
|
||||
"AccountActivities": "Активность учетной записи",
|
||||
"AccountAndPasswordChangeRank": "Рейтинг изменений паролей и учётных записей",
|
||||
@@ -46,7 +44,6 @@
|
||||
"AccountPushUpdate": "Обновление УЗ для публикации",
|
||||
"AccountReport": "Отчет по УЗ",
|
||||
"AccountResult": "Успешное или неудачное изменение секрета УЗ",
|
||||
"AccountSecretReadDisabled": "Функция чтения логина и пароля была отключена администратором",
|
||||
"AccountSelectHelpText": "В списке учетных записей отображается имя пользователя",
|
||||
"AccountSessions": "Сессии учетной записи",
|
||||
"AccountStatisticsReport": "Отчет по учетным записям",
|
||||
@@ -282,7 +279,6 @@
|
||||
"CACertificate": "Сертификат ЦС",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "Tianyi Cloud",
|
||||
"CTYunPrivate": "eCloud Private Cloud",
|
||||
"CalculationResults": "Ошибка в выражении cron",
|
||||
"CallRecords": "Запись вызовов",
|
||||
@@ -1180,8 +1176,6 @@
|
||||
"RetrySelected": "Повторить выбранное",
|
||||
"Review": "Требовать одобрения",
|
||||
"Reviewer": "Утверждающий",
|
||||
"Revoke": "Отмена",
|
||||
"Risk": "Риск",
|
||||
"RiskDetection": "Выявление рисков",
|
||||
"RiskDetectionDetail": "Детали обнаружения риска",
|
||||
"RiskyAccount": "УЗ с риском",
|
||||
@@ -1645,7 +1639,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "Это действие заменит все протоколы и порты. Продолжить?",
|
||||
"pleaseSelectAssets": "Пожалуйста, выберите актив",
|
||||
"removeWarningMsg": "Вы уверены, что хотите удалить",
|
||||
"selectFiles": "Выбрано {number} файлов",
|
||||
"selectedAssets": "Выбранные активы",
|
||||
"setVariable": "Задать переменную",
|
||||
"userId": "ID пользователя",
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "Phân bố truy cập",
|
||||
"AccessIP": "Danh sách trắng IP",
|
||||
"AccessKey": "Khóa truy cập",
|
||||
"AccessToken": "Mã thông hành",
|
||||
"AccessTokenTip": "Mã thông hành là giấy chứng nhận tạm thời được tạo ra thông qua quy trình OAuth2 (ủy quyền mã) sử dụng khách hàng JumpServer, dùng để truy cập tài nguyên được bảo vệ.",
|
||||
"Account": "Tài khoản",
|
||||
"AccountActivities": "Tài khoản hoạt động",
|
||||
"AccountAndPasswordChangeRank": "Thay đổi mật khẩu tài khoản xếp hạng",
|
||||
@@ -46,7 +44,6 @@
|
||||
"AccountPushUpdate": "Cập nhật thông tin tài khoản",
|
||||
"AccountReport": "Báo cáo tài khoản",
|
||||
"AccountResult": "Thay đổi mật khẩu tài khoản thành công/thất bại",
|
||||
"AccountSecretReadDisabled": "Chức năng đọc tài khoản mật khẩu đã bị quản lý bởi quản trị viên vô hiệu hóa",
|
||||
"AccountSelectHelpText": "Danh sách tài khoản thêm tên người dùng của tài khoản",
|
||||
"AccountSessions": "Phiên tài khoản",
|
||||
"AccountStatisticsReport": "Báo cáo thống kê tài khoản",
|
||||
@@ -282,7 +279,6 @@
|
||||
"CACertificate": "Chứng chỉ CA",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "Điện toán đám mây Thiên Vân",
|
||||
"CTYunPrivate": "Đám mây riêng Tianyi",
|
||||
"CalculationResults": "Biểu thức cron sai",
|
||||
"CallRecords": "Ghi chép gọi",
|
||||
@@ -1180,8 +1176,6 @@
|
||||
"RetrySelected": "Thử lại đã chọn",
|
||||
"Review": "Xem xét",
|
||||
"Reviewer": "Người phê duyệt",
|
||||
"Revoke": "Huỷ bỏ",
|
||||
"Risk": "Rủi ro",
|
||||
"RiskDetection": "Phát hiện rủi ro",
|
||||
"RiskDetectionDetail": "Chi tiết phát hiện rủi ro",
|
||||
"RiskyAccount": "Tài khoản rủi ro",
|
||||
@@ -1645,7 +1639,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "Hành động này sẽ ghi đè lên tất cả các giao thức và cổng, có tiếp tục không?",
|
||||
"pleaseSelectAssets": "Vui lòng chọn tài sản.",
|
||||
"removeWarningMsg": "Bạn có chắc chắn muốn xóa bỏ?",
|
||||
"selectFiles": "Đã chọn chọn {number} tệp tin",
|
||||
"selectedAssets": "Tài sản đã chọn",
|
||||
"setVariable": "Cài đặt tham số",
|
||||
"userId": "ID người dùng",
|
||||
|
||||
@@ -1648,6 +1648,5 @@
|
||||
"selectFiles": "已选择选择{number}文件",
|
||||
"AccessToken": "访问令牌",
|
||||
"AccessTokenTip": "访问令牌是通过 JumpServer 客户端使用 OAuth2(授权码授权)流程生成的临时凭证,用于访问受保护的资源。",
|
||||
"Revoke": "撤销",
|
||||
"AccountSecretReadDisabled": "账号密码读取功能已被管理员禁用"
|
||||
}
|
||||
"Revoke": "撤销"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"AccessDistribution": "訪問分布",
|
||||
"AccessIP": "IP 白名單",
|
||||
"AccessKey": "訪問金鑰",
|
||||
"AccessToken": "訪問令牌",
|
||||
"AccessTokenTip": "訪問令牌是透過 JumpServer 客戶端使用 OAuth2(授權碼授權)流程生成的臨時憑證,用於訪問受保護的資源。",
|
||||
"Account": "雲帳號",
|
||||
"AccountActivities": "帳號活動",
|
||||
"AccountAmount": "帳號數量",
|
||||
@@ -48,7 +46,6 @@
|
||||
"AccountPushUpdate": "更新帳號推送",
|
||||
"AccountReport": "帳號報表",
|
||||
"AccountResult": "帳號改密成功/失敗",
|
||||
"AccountSecretReadDisabled": "帳號密碼讀取功能已被管理員禁用",
|
||||
"AccountSelectHelpText": "帳號清單所加入的是使用者名稱",
|
||||
"AccountSessions": "帳號會話",
|
||||
"AccountStatisticsReport": "帳號統計報告",
|
||||
@@ -286,7 +283,6 @@
|
||||
"CACertificate": "CA 證書",
|
||||
"CAS": "CAS",
|
||||
"CMPP2": "CMPP v2.0",
|
||||
"CTYun": "天翼雲",
|
||||
"CTYunPrivate": "天翼私有雲",
|
||||
"CalculationResults": "呼叫記錄",
|
||||
"CallRecords": "調用記錄",
|
||||
@@ -1185,8 +1181,6 @@
|
||||
"RetrySelected": "重新嘗試所選",
|
||||
"Review": "審查",
|
||||
"Reviewer": "審批人",
|
||||
"Revoke": "撤銷",
|
||||
"Risk": "風險",
|
||||
"RiskDetection": "風險檢測",
|
||||
"RiskDetectionDetail": "風險檢測詳情",
|
||||
"RiskyAccount": "風險帳號",
|
||||
@@ -1650,7 +1644,6 @@
|
||||
"overwriteProtocolsAndPortsMsg": "此操作將覆蓋所有協議和端口,是否繼續?",
|
||||
"pleaseSelectAssets": "請選擇資產",
|
||||
"removeWarningMsg": "你確定要移除",
|
||||
"selectFiles": "已選擇{number}文件",
|
||||
"selectedAssets": "已選資產",
|
||||
"setVariable": "設置參數",
|
||||
"userId": "用戶ID",
|
||||
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "Start time",
|
||||
"success": "Success",
|
||||
"system user": "System user",
|
||||
"user": "User",
|
||||
"tabLimits": "15 tabs are currently open.\nTo ensure system stability, would you like to open Luna in a new browser tab to continue?"
|
||||
"user": "User"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "Hora de inicio",
|
||||
"success": "Éxito",
|
||||
"system user": "Usuario del sistema",
|
||||
"tabLimits": "Actualmente tienes 15 pestañas abiertas. \n¿Para garantizar la estabilidad del sistema, deberías abrir Luna en una nueva pestaña del navegador para continuar con la operación?",
|
||||
"user": "Usuario"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "開始時間",
|
||||
"success": "成功",
|
||||
"system user": "システムユーザー",
|
||||
"tabLimits": "現在、15個のタブが開かれています。システムの安定性を確保するために、新しいブラウザのタブでLunaを開いて操作を続けますか?",
|
||||
"user": "ユーザー"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "시작 시간",
|
||||
"success": "성공",
|
||||
"system user": "시스템 사용자",
|
||||
"tabLimits": "현재 15개의 탭이 열려 있습니다. 시스템의 안정성을 위해 Luna를 계속 사용하려면 새로운 브라우저 탭에서 열어보시겠습니까?",
|
||||
"user": "사용자"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "Hora de início",
|
||||
"success": " Sucesso",
|
||||
"system user": "Usuário do Sistema",
|
||||
"tabLimits": "Atualmente, 15 abas estão abertas. Para garantir a estabilidade do sistema, você deseja abrir o Luna em uma nova aba do navegador para continuar a operação?",
|
||||
"user": "Usuário"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "время начала",
|
||||
"success": "успешно",
|
||||
"system user": "системный пользователь",
|
||||
"tabLimits": "В данный момент открыто 15 вкладок. \nЧтобы обеспечить стабильность системы, стоит ли открыть Luna в новой вкладке браузера для продолжения работы?",
|
||||
"user": "пользователь"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "Thời gian bắt đầu",
|
||||
"success": "Thành công",
|
||||
"system user": "Tên đăng nhập",
|
||||
"tabLimits": "Hiện tại đã mở 15 tab. \nĐể đảm bảo tính ổn định của hệ thống, có qua tab trình duyệt mới để mở Luna và tiếp tục thao tác không?",
|
||||
"user": "Người dùng"
|
||||
}
|
||||
@@ -288,6 +288,5 @@
|
||||
"start time": "开始时间",
|
||||
"success": "成功",
|
||||
"system user": "系统用户",
|
||||
"user": "用户",
|
||||
"tabLimits": "当前已打开 15 个标签页。\n为保证系统稳定是否在新的浏览器标签页中打开 Luna 以继续操作?"
|
||||
"user": "用户"
|
||||
}
|
||||
@@ -289,6 +289,5 @@
|
||||
"start time": "開始時間",
|
||||
"success": "成功",
|
||||
"system user": "系統用戶",
|
||||
"tabLimits": "當前已打開 15 個標籤頁。 \n為了確保系統穩定,是否在新的瀏覽器標籤頁中打開 Luna 以繼續操作?",
|
||||
"user": "用戶"
|
||||
}
|
||||
@@ -13,11 +13,11 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from importlib import import_module
|
||||
from urllib.parse import urljoin, urlparse, quote
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -575,7 +575,6 @@ class Config(dict):
|
||||
],
|
||||
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': 'auto',
|
||||
'SECURITY_VIEW_AUTH_NEED_MFA': True,
|
||||
'SECURITY_ACCOUNT_SECRET_READ': True,
|
||||
'SECURITY_MAX_IDLE_TIME': 30,
|
||||
'SECURITY_MAX_SESSION_TIME': 24,
|
||||
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
|
||||
@@ -699,21 +698,10 @@ class Config(dict):
|
||||
'LIMIT_SUPER_PRIV': False,
|
||||
|
||||
# Chat AI
|
||||
'IS_CUSTOM_MODEL': False,
|
||||
'CHAT_AI_ENABLED': False,
|
||||
'CHAT_AI_METHOD': 'api',
|
||||
'CHAT_AI_EMBED_URL': '',
|
||||
'CHAT_AI_TYPE': 'gpt',
|
||||
'GPT_BASE_URL': '',
|
||||
'GPT_API_KEY': '',
|
||||
'GPT_PROXY': '',
|
||||
'GPT_MODEL': 'gpt-4o-mini',
|
||||
'CUSTOM_GPT_MODEL': 'gpt-4o-mini',
|
||||
'DEEPSEEK_BASE_URL': '',
|
||||
'DEEPSEEK_API_KEY': '',
|
||||
'DEEPSEEK_PROXY': '',
|
||||
'DEEPSEEK_MODEL': 'deepseek-chat',
|
||||
'CUSTOM_DEEPSEEK_MODEL': 'deepseek-chat',
|
||||
'CHAT_AI_PROVIDERS': [],
|
||||
'VIRTUAL_APP_ENABLED': False,
|
||||
|
||||
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
|
||||
@@ -721,6 +709,11 @@ class Config(dict):
|
||||
'TICKET_APPLY_ASSET_SCOPE': 'all',
|
||||
'LEAK_PASSWORD_DB_PATH': os.path.join(PROJECT_DIR, 'data', 'system', 'leak_passwords.db'),
|
||||
|
||||
# Ansible Receptor
|
||||
'RECEPTOR_ENABLED': False,
|
||||
'ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST': 'jms_celery',
|
||||
'ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS': 'receptor:7521',
|
||||
|
||||
'FILE_UPLOAD_TEMP_DIR': None,
|
||||
|
||||
'LOKI_LOG_ENABLED': False,
|
||||
|
||||
@@ -60,7 +60,6 @@ VERIFY_CODE_TTL = CONFIG.VERIFY_CODE_TTL
|
||||
SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL
|
||||
SECURITY_UNCOMMON_USERS_TTL = CONFIG.SECURITY_UNCOMMON_USERS_TTL
|
||||
SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA
|
||||
SECURITY_ACCOUNT_SECRET_READ = CONFIG.SECURITY_ACCOUNT_SECRET_READ
|
||||
SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION
|
||||
SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED
|
||||
SECURITY_MFA_IN_LOGIN_PAGE = CONFIG.SECURITY_MFA_IN_LOGIN_PAGE
|
||||
@@ -239,21 +238,10 @@ LIMIT_SUPER_PRIV = CONFIG.LIMIT_SUPER_PRIV
|
||||
ASSET_SIZE = 'small'
|
||||
|
||||
# Chat AI
|
||||
IS_CUSTOM_MODEL = CONFIG.IS_CUSTOM_MODEL
|
||||
CUSTOM_GPT_MODEL = CONFIG.CUSTOM_GPT_MODEL
|
||||
CUSTOM_DEEPSEEK_MODEL = CONFIG.CUSTOM_DEEPSEEK_MODEL
|
||||
CHAT_AI_ENABLED = CONFIG.CHAT_AI_ENABLED
|
||||
CHAT_AI_METHOD = CONFIG.CHAT_AI_METHOD
|
||||
CHAT_AI_EMBED_URL = CONFIG.CHAT_AI_EMBED_URL
|
||||
CHAT_AI_TYPE = CONFIG.CHAT_AI_TYPE
|
||||
GPT_BASE_URL = CONFIG.GPT_BASE_URL
|
||||
GPT_API_KEY = CONFIG.GPT_API_KEY
|
||||
GPT_PROXY = CONFIG.GPT_PROXY
|
||||
GPT_MODEL = CONFIG.GPT_MODEL
|
||||
DEEPSEEK_BASE_URL = CONFIG.DEEPSEEK_BASE_URL
|
||||
DEEPSEEK_API_KEY = CONFIG.DEEPSEEK_API_KEY
|
||||
DEEPSEEK_PROXY = CONFIG.DEEPSEEK_PROXY
|
||||
DEEPSEEK_MODEL = CONFIG.DEEPSEEK_MODEL
|
||||
CHAT_AI_DEFAULT_PROVIDER = CONFIG.CHAT_AI_DEFAULT_PROVIDER
|
||||
|
||||
VIRTUAL_APP_ENABLED = CONFIG.VIRTUAL_APP_ENABLED
|
||||
|
||||
@@ -261,6 +249,11 @@ FILE_UPLOAD_SIZE_LIMIT_MB = CONFIG.FILE_UPLOAD_SIZE_LIMIT_MB
|
||||
|
||||
TICKET_APPLY_ASSET_SCOPE = CONFIG.TICKET_APPLY_ASSET_SCOPE
|
||||
|
||||
# Ansible Receptor
|
||||
RECEPTOR_ENABLED = CONFIG.RECEPTOR_ENABLED
|
||||
ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST = CONFIG.ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST
|
||||
ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS = CONFIG.ANSIBLE_RECEPTOR_TCP_LISTEN_ADDRESS
|
||||
|
||||
LOKI_LOG_ENABLED = CONFIG.LOKI_LOG_ENABLED
|
||||
LOKI_BASE_URL = CONFIG.LOKI_BASE_URL
|
||||
|
||||
@@ -268,3 +261,5 @@ TOOL_USER_ENABLED = CONFIG.TOOL_USER_ENABLED
|
||||
|
||||
SUGGESTION_LIMIT = CONFIG.SUGGESTION_LIMIT
|
||||
MCP_ENABLED = CONFIG.MCP_ENABLED
|
||||
CHAT_AI_PROVIDERS = CONFIG.CHAT_AI_PROVIDERS
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#
|
||||
import os
|
||||
import time
|
||||
|
||||
from .base import (
|
||||
REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL,
|
||||
REDIS_PROTOCOL, REDIS_SENTINEL_SERVICE_NAME, REDIS_SENTINELS, REDIS_SENTINEL_PASSWORD,
|
||||
@@ -31,8 +30,8 @@ REST_FRAMEWORK = {
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
# 'rest_framework.authentication.BasicAuthentication',
|
||||
'authentication.backends.drf.ServiceAuthentication',
|
||||
'authentication.backends.drf.SignatureAuthentication',
|
||||
'authentication.backends.drf.ServiceAuthentication',
|
||||
'authentication.backends.drf.PrivateTokenAuthentication',
|
||||
'authentication.backends.drf.AccessTokenAuthentication',
|
||||
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
|
||||
|
||||
@@ -115,11 +115,7 @@ LOGGING = {
|
||||
'azure': {
|
||||
'handlers': ['null'],
|
||||
'level': 'ERROR'
|
||||
},
|
||||
'oauth2_provider': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class ResourceDownload(TemplateView):
|
||||
OPENSSH_VERSION=v9.4.0.0
|
||||
TINKER_VERSION=v0.1.6
|
||||
VIDEO_PLAYER_VERSION=0.6.0
|
||||
CLIENT_VERSION=4.1.0
|
||||
CLIENT_VERSION=4.0.0
|
||||
"""
|
||||
|
||||
def get_meta_json(self):
|
||||
|
||||
@@ -28,7 +28,7 @@ is_available:
|
||||
sample: true
|
||||
'''
|
||||
|
||||
import telnetlib
|
||||
import telnetlib3
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
@@ -57,9 +57,9 @@ def main():
|
||||
port = module.params['login_port']
|
||||
timeout = module.params['timeout']
|
||||
try:
|
||||
client = telnetlib.Telnet(host, port, timeout=timeout)
|
||||
client = telnetlib3.Telnet(host, port, timeout=timeout)
|
||||
client.close()
|
||||
except Exception as err: # noqa
|
||||
except Exception as err: # noqa
|
||||
result['is_available'] = False
|
||||
module.fail_json(msg='Unable to connect to asset: %s' % err)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.utils.functional import LazyObject
|
||||
|
||||
from ops.ansible import AnsibleNativeRunner
|
||||
@@ -14,7 +15,9 @@ class _LazyRunnerInterface(LazyObject):
|
||||
@staticmethod
|
||||
def make_interface():
|
||||
runner_type = AnsibleNativeRunner
|
||||
return RunnerInterface(runner_type=runner_type, gateway_proxy_host='127.0.0.1')
|
||||
gateway_host = settings.ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST \
|
||||
if settings.ANSIBLE_RECEPTOR_GATEWAY_PROXY_HOST else '127.0.0.1'
|
||||
return RunnerInterface(runner_type=runner_type, gateway_proxy_host=gateway_host)
|
||||
|
||||
|
||||
interface = _LazyRunnerInterface()
|
||||
|
||||
@@ -1,104 +1,10 @@
|
||||
import httpx
|
||||
import openai
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidUser, OnlySuperUser
|
||||
from .. import serializers
|
||||
from ..const import ChatAITypeChoices
|
||||
from ..models import ChatPrompt
|
||||
from ..prompt import DefaultChatPrompt
|
||||
|
||||
|
||||
class ChatAITestingAPI(GenericAPIView):
|
||||
serializer_class = serializers.ChatAISettingSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'settings.change_chatai'
|
||||
}
|
||||
|
||||
def get_config(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = self.serializer_class().data
|
||||
data.update(serializer.validated_data)
|
||||
for k, v in data.items():
|
||||
if v:
|
||||
continue
|
||||
# 页面没有传递值, 从 settings 中获取
|
||||
data[k] = getattr(settings, k, None)
|
||||
return data
|
||||
|
||||
def post(self, request):
|
||||
config = self.get_config(request)
|
||||
chat_ai_enabled = config['CHAT_AI_ENABLED']
|
||||
if not chat_ai_enabled:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={'msg': _('Chat AI is not enabled')}
|
||||
)
|
||||
|
||||
tp = config['CHAT_AI_TYPE']
|
||||
is_custom_model = config['IS_CUSTOM_MODEL']
|
||||
if tp == ChatAITypeChoices.gpt:
|
||||
url = config['GPT_BASE_URL']
|
||||
api_key = config['GPT_API_KEY']
|
||||
proxy = config['GPT_PROXY']
|
||||
model = config['GPT_MODEL']
|
||||
custom_model = config['CUSTOM_GPT_MODEL']
|
||||
else:
|
||||
url = config['DEEPSEEK_BASE_URL']
|
||||
api_key = config['DEEPSEEK_API_KEY']
|
||||
proxy = config['DEEPSEEK_PROXY']
|
||||
model = config['DEEPSEEK_MODEL']
|
||||
custom_model = config['CUSTOM_DEEPSEEK_MODEL']
|
||||
|
||||
model = custom_model or model \
|
||||
if is_custom_model else model
|
||||
|
||||
kwargs = {
|
||||
'base_url': url or None,
|
||||
'api_key': api_key,
|
||||
}
|
||||
try:
|
||||
if proxy:
|
||||
kwargs['http_client'] = httpx.Client(
|
||||
proxies=proxy,
|
||||
transport=httpx.HTTPTransport(local_address='0.0.0.0')
|
||||
)
|
||||
client = openai.OpenAI(**kwargs)
|
||||
|
||||
ok = False
|
||||
error = ''
|
||||
|
||||
client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Say this is a test",
|
||||
}
|
||||
],
|
||||
model=model,
|
||||
)
|
||||
ok = True
|
||||
except openai.APIConnectionError as e:
|
||||
error = str(e.__cause__) # an underlying Exception, likely raised within httpx.
|
||||
except openai.APIStatusError as e:
|
||||
error = str(e.message)
|
||||
except Exception as e:
|
||||
ok, error = False, str(e)
|
||||
|
||||
if ok:
|
||||
_status, msg = status.HTTP_200_OK, _('Test success')
|
||||
else:
|
||||
_status, msg = status.HTTP_400_BAD_REQUEST, error
|
||||
|
||||
return Response(status=_status, data={'msg': msg})
|
||||
|
||||
|
||||
class ChatPromptViewSet(JMSModelViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.ChatPromptSerializer,
|
||||
|
||||
@@ -154,7 +154,10 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||
def parse_serializer_data(self, serializer):
|
||||
data = []
|
||||
fields = self.get_fields()
|
||||
encrypted_items = [name for name, field in fields.items() if field.write_only]
|
||||
encrypted_items = [
|
||||
name for name, field in fields.items()
|
||||
if field.write_only or getattr(field, 'encrypted', False)
|
||||
]
|
||||
category = self.request.query_params.get('category', '')
|
||||
for name, value in serializer.validated_data.items():
|
||||
encrypted = name in encrypted_items
|
||||
@@ -222,4 +225,4 @@ class ClientVersionView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response(['4.0.0', '4.1.0'], status=status.HTTP_200_OK)
|
||||
return Response(['4.0.0'], status=status.HTTP_200_OK)
|
||||
|
||||
@@ -14,30 +14,5 @@ class ChatAIMethodChoices(TextChoices):
|
||||
|
||||
|
||||
class ChatAITypeChoices(TextChoices):
|
||||
gpt = 'gpt', 'GPT'
|
||||
deep_seek = 'deep-seek', 'DeepSeek'
|
||||
|
||||
|
||||
class GPTModelChoices(TextChoices):
|
||||
# 🚀 Latest flagship dialogue model
|
||||
GPT_5_2 = 'gpt-5.2', 'gpt-5.2'
|
||||
GPT_5_2_PRO = 'gpt-5.2-pro', 'gpt-5.2-pro'
|
||||
|
||||
GPT_5_1 = 'gpt-5.1', 'gpt-5.1'
|
||||
GPT_5 = 'gpt-5', 'gpt-5'
|
||||
|
||||
# 💡 Lightweight & Cost-Friendly Version
|
||||
GPT_5_MINI = 'gpt-5-mini', 'gpt-5-mini'
|
||||
GPT_5_NANO = 'gpt-5-nano', 'gpt-5-nano'
|
||||
|
||||
# 🧠 GPT-4 series of dialogues (still supports chat tasks)
|
||||
GPT_4O = 'gpt-4o', 'gpt-4o'
|
||||
GPT_4O_MINI = 'gpt-4o-mini', 'gpt-4o-mini'
|
||||
GPT_4_1 = 'gpt-4.1', 'gpt-4.1'
|
||||
GPT_4_1_MINI = 'gpt-4.1-mini', 'gpt-4.1-mini'
|
||||
GPT_4_1_NANO = 'gpt-4.1-nano', 'gpt-4.1-nano'
|
||||
|
||||
|
||||
class DeepSeekModelChoices(TextChoices):
|
||||
deepseek_chat = 'deepseek-chat', 'DeepSeek-V3'
|
||||
deepseek_reasoner = 'deepseek-reasoner', 'DeepSeek-R1'
|
||||
openai = 'openai', 'Openai'
|
||||
ollama = 'ollama', 'Ollama'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
@@ -14,7 +15,6 @@ from rest_framework.utils.encoders import JSONEncoder
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.db.utils import Encryptor
|
||||
from common.utils import get_logger
|
||||
from .const import ChatAITypeChoices
|
||||
from .signals import setting_changed
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -196,24 +196,25 @@ class ChatPrompt(JMSBaseModel):
|
||||
return self.name
|
||||
|
||||
|
||||
def get_chatai_data():
|
||||
data = {
|
||||
'url': settings.GPT_BASE_URL,
|
||||
'api_key': settings.GPT_API_KEY,
|
||||
'proxy': settings.GPT_PROXY,
|
||||
'model': settings.GPT_MODEL,
|
||||
}
|
||||
custom_model = settings.CUSTOM_GPT_MODEL
|
||||
if settings.CHAT_AI_TYPE != ChatAITypeChoices.gpt:
|
||||
data['url'] = settings.DEEPSEEK_BASE_URL
|
||||
data['api_key'] = settings.DEEPSEEK_API_KEY
|
||||
data['proxy'] = settings.DEEPSEEK_PROXY
|
||||
data['model'] = settings.DEEPSEEK_MODEL
|
||||
custom_model = settings.CUSTOM_DEEPSEEK_MODEL
|
||||
def get_chatai_data() -> Dict[str, Any]:
|
||||
raw_providers = settings.CHAT_AI_PROVIDERS
|
||||
providers: List[dict] = [p for p in raw_providers if isinstance(p, dict)]
|
||||
|
||||
data['model'] = custom_model or data['model'] \
|
||||
if settings.IS_CUSTOM_MODEL else data['model']
|
||||
return data
|
||||
if not providers:
|
||||
return {}
|
||||
|
||||
selected = next(
|
||||
(p for p in providers if p.get('is_assistant')),
|
||||
providers[0],
|
||||
)
|
||||
|
||||
return {
|
||||
'url': selected.get('base_url'),
|
||||
'api_key': selected.get('api_key'),
|
||||
'proxy': selected.get('proxy'),
|
||||
'model': selected.get('model'),
|
||||
'name': selected.get('name'),
|
||||
}
|
||||
|
||||
|
||||
def init_sqlite_db():
|
||||
|
||||
@@ -10,11 +10,12 @@ from common.utils import date_expired_default
|
||||
__all__ = [
|
||||
'AnnouncementSettingSerializer', 'OpsSettingSerializer', 'VaultSettingSerializer',
|
||||
'HashicorpKVSerializer', 'AzureKVSerializer', 'TicketSettingSerializer',
|
||||
'ChatAISettingSerializer', 'VirtualAppSerializer', 'AmazonSMSerializer',
|
||||
'ChatAIProviderSerializer', 'ChatAISettingSerializer',
|
||||
'VirtualAppSerializer', 'AmazonSMSerializer',
|
||||
]
|
||||
|
||||
from settings.const import (
|
||||
ChatAITypeChoices, GPTModelChoices, DeepSeekModelChoices, ChatAIMethodChoices
|
||||
ChatAITypeChoices, ChatAIMethodChoices
|
||||
)
|
||||
|
||||
|
||||
@@ -120,6 +121,29 @@ class AmazonSMSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
|
||||
class ChatAIProviderListSerializer(serializers.ListSerializer):
|
||||
# 标记整个列表需要加密存储,避免明文保存 API Key
|
||||
encrypted = True
|
||||
|
||||
|
||||
class ChatAIProviderSerializer(serializers.Serializer):
|
||||
type = serializers.ChoiceField(
|
||||
default=ChatAITypeChoices.openai, choices=ChatAITypeChoices.choices,
|
||||
label=_("Types"), required=False,
|
||||
)
|
||||
base_url = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Base URL'),
|
||||
help_text=_('The base URL of the Chat service.')
|
||||
)
|
||||
api_key = EncryptedField(
|
||||
allow_blank=True, required=False, label=_('API Key'),
|
||||
)
|
||||
proxy = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Proxy'),
|
||||
help_text=_('The proxy server address of the GPT service. For example: http://ip:port')
|
||||
)
|
||||
|
||||
|
||||
class ChatAISettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('Chat AI')
|
||||
|
||||
@@ -130,56 +154,14 @@ class ChatAISettingSerializer(serializers.Serializer):
|
||||
default=ChatAIMethodChoices.api, choices=ChatAIMethodChoices.choices,
|
||||
label=_("Method"), required=False,
|
||||
)
|
||||
CHAT_AI_PROVIDERS = ChatAIProviderListSerializer(
|
||||
child=ChatAIProviderSerializer(),
|
||||
allow_empty=True, required=False, default=list, label=_('Providers')
|
||||
)
|
||||
CHAT_AI_EMBED_URL = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Base URL'),
|
||||
help_text=_('The base URL of the Chat service.')
|
||||
)
|
||||
CHAT_AI_TYPE = serializers.ChoiceField(
|
||||
default=ChatAITypeChoices.gpt, choices=ChatAITypeChoices.choices,
|
||||
label=_("Types"), required=False,
|
||||
)
|
||||
GPT_BASE_URL = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Base URL'),
|
||||
help_text=_('The base URL of the Chat service.')
|
||||
)
|
||||
GPT_API_KEY = EncryptedField(
|
||||
allow_blank=True, required=False, label=_('API Key'),
|
||||
)
|
||||
GPT_PROXY = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Proxy'),
|
||||
help_text=_('The proxy server address of the GPT service. For example: http://ip:port')
|
||||
)
|
||||
GPT_MODEL = serializers.ChoiceField(
|
||||
default=GPTModelChoices.GPT_4_1_MINI, choices=GPTModelChoices.choices,
|
||||
label=_("GPT Model"), required=False,
|
||||
)
|
||||
DEEPSEEK_BASE_URL = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Base URL'),
|
||||
help_text=_('The base URL of the Chat service.')
|
||||
)
|
||||
DEEPSEEK_API_KEY = EncryptedField(
|
||||
allow_blank=True, required=False, label=_('API Key'),
|
||||
)
|
||||
DEEPSEEK_PROXY = serializers.CharField(
|
||||
allow_blank=True, required=False, label=_('Proxy'),
|
||||
help_text=_('The proxy server address of the GPT service. For example: http://ip:port')
|
||||
)
|
||||
DEEPSEEK_MODEL = serializers.ChoiceField(
|
||||
default=DeepSeekModelChoices.deepseek_chat, choices=DeepSeekModelChoices.choices,
|
||||
label=_("DeepSeek Model"), required=False,
|
||||
)
|
||||
IS_CUSTOM_MODEL = serializers.BooleanField(
|
||||
required=False, default=False, label=_("Custom Model"),
|
||||
help_text=_("Whether to use a custom model")
|
||||
)
|
||||
CUSTOM_GPT_MODEL = serializers.CharField(
|
||||
max_length=256, allow_blank=True,
|
||||
required=False, label=_('Custom gpt model'),
|
||||
)
|
||||
CUSTOM_DEEPSEEK_MODEL = serializers.CharField(
|
||||
max_length=256, allow_blank=True,
|
||||
required=False, label=_('Custom DeepSeek model'),
|
||||
)
|
||||
|
||||
|
||||
class TicketSettingSerializer(serializers.Serializer):
|
||||
|
||||
@@ -21,7 +21,6 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
TICKET_AUTHORIZE_DEFAULT_TIME_UNIT = serializers.CharField()
|
||||
AUTH_LDAP_SYNC_ORG_IDS = serializers.ListField()
|
||||
SECURITY_MAX_IDLE_TIME = serializers.IntegerField()
|
||||
SECURITY_ACCOUNT_SECRET_READ = serializers.BooleanField()
|
||||
SECURITY_VIEW_AUTH_NEED_MFA = serializers.BooleanField()
|
||||
SECURITY_MFA_AUTH = serializers.IntegerField()
|
||||
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField()
|
||||
@@ -74,8 +73,6 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
CHAT_AI_ENABLED = serializers.BooleanField()
|
||||
CHAT_AI_METHOD = serializers.CharField()
|
||||
CHAT_AI_EMBED_URL = serializers.CharField()
|
||||
CHAT_AI_TYPE = serializers.CharField()
|
||||
GPT_MODEL = serializers.CharField()
|
||||
FILE_UPLOAD_SIZE_LIMIT_MB = serializers.IntegerField()
|
||||
FTP_FILE_MAX_STORE = serializers.IntegerField()
|
||||
LOKI_LOG_ENABLED = serializers.BooleanField()
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
#
|
||||
import asyncio
|
||||
import socket
|
||||
import telnetlib
|
||||
|
||||
import telnetlib3
|
||||
|
||||
from settings.utils import generate_ips
|
||||
|
||||
@@ -12,7 +13,7 @@ PROMPT_REGEX = r'[\<|\[](.*)[\>|\]]'
|
||||
async def telnet(dest_addr, port_number=23, timeout=10):
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
connection = await loop.run_in_executor(None, telnetlib.Telnet, dest_addr, port_number, timeout)
|
||||
connection = await loop.run_in_executor(None, telnetlib3.Telnet, dest_addr, port_number, timeout)
|
||||
except asyncio.TimeoutError:
|
||||
return False, 'Timeout'
|
||||
except (ConnectionRefusedError, socket.timeout, socket.gaierror) as e:
|
||||
|
||||
@@ -21,7 +21,6 @@ urlpatterns = [
|
||||
path('sms/<str:backend>/testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
|
||||
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
|
||||
path('vault/<str:backend>/testing/', api.VaultTestingAPI.as_view(), name='vault-testing'),
|
||||
path('chatai/testing/', api.ChatAITestingAPI.as_view(), name='chatai-testing'),
|
||||
path('vault/sync/', api.VaultSyncDataAPI.as_view(), name='vault-sync'),
|
||||
path('security/block-ip/', api.BlockIPSecurityAPI.as_view(), name='block-ip'),
|
||||
path('security/unlock-ip/', api.UnlockIPSecurityAPI.as_view(), name='unlock-ip'),
|
||||
|
||||
@@ -18,28 +18,40 @@
|
||||
}
|
||||
|
||||
/* 重定向中的样式 */
|
||||
.redirecting-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.redirecting-container.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.confirm-container {
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
.confirm-container.hide {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<!-- 确认内容 -->
|
||||
<div class="confirm-container" id="confirmContainer">
|
||||
<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.' %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% trans 'Please confirm that you trust this link: ' %}
|
||||
<br/>
|
||||
<br/>
|
||||
<a class="target-url" href="javascript:void(0)" onclick="handleRedirect(event)">{{ target_url }}</a>
|
||||
</div>
|
||||
<div class="alert {% if error %} alert-danger {% else %} alert-info {% endif %}" id="messages">
|
||||
{% trans 'You are about to be redirected to an external website.' %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% trans 'Please confirm that you trust this link: ' %}
|
||||
<br/>
|
||||
<br/>
|
||||
<a class="target-url" href="javascript:void(0)" onclick="handleRedirect(event)">{{ target_url }}</a>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<a id="backBtn" href="/" class="btn btn-default block full-width m-b">
|
||||
<a href="/" class="btn btn-default block full-width m-b">
|
||||
{% trans 'Back' %}
|
||||
</a>
|
||||
</div>
|
||||
@@ -50,60 +62,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重定向中内容 -->
|
||||
<div class="redirecting-container" id="redirectingContainer">
|
||||
<p>
|
||||
<div class="alert alert-info" id="messages">
|
||||
{% trans 'Redirecting you to the Desktop App ( JumpServer Client )' %}
|
||||
<br/>
|
||||
<br/>
|
||||
{% trans 'You can safely close this window and return to the application.' %}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
if (sessionStorage.getItem('page_refreshed')) {
|
||||
// 用户刷新了页面,清除标记并跳转到首页
|
||||
sessionStorage.removeItem('page_refreshed');
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
// 第一次加载,设置标记
|
||||
window.addEventListener('beforeunload', function() {
|
||||
sessionStorage.setItem('page_refreshed', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
const targetUrl = '{{ target_url }}';
|
||||
|
||||
let countdownTimer = null;
|
||||
let countdownSeconds = 30;
|
||||
let startedCountdown = false;
|
||||
// 判断是否是 jms:// 协议
|
||||
function isJmsProtocol(url) {
|
||||
return url.toLowerCase().startsWith('jms://');
|
||||
}
|
||||
|
||||
function handleRedirect(event) {
|
||||
// 如果有 event,阻止默认行为
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (isJmsProtocol(targetUrl)) {
|
||||
// 隐藏确认内容
|
||||
document.getElementById('confirmContainer').classList.add('hide');
|
||||
// 显示重定向中
|
||||
document.getElementById('redirectingContainer').classList.add('show');
|
||||
}
|
||||
|
||||
// 延迟后执行跳转(让用户看到加载动画)
|
||||
setTimeout(() => {
|
||||
if (targetUrl.startsWith('jms://')) {
|
||||
if (startedCountdown) {
|
||||
// 已经开始倒计时,直接跳转但不再重置倒计时
|
||||
window.location.href = targetUrl;
|
||||
return;
|
||||
}
|
||||
startedCountdown = true;
|
||||
countdownSeconds = 30;
|
||||
window.location.href = targetUrl;
|
||||
const backBtn = document.getElementById('backBtn');
|
||||
if (backBtn) {
|
||||
const backButtonContent = backBtn.textContent;
|
||||
backBtn.textContent = `${backButtonContent} (${countdownSeconds})`;
|
||||
countdownTimer = setInterval(() => {
|
||||
countdownSeconds--;
|
||||
if (countdownSeconds > 0) {
|
||||
backBtn.textContent = `${backButtonContent} (${countdownSeconds})`;
|
||||
} else {
|
||||
backBtn.textContent = `${backButtonContent}`;
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
window.location.href = targetUrl;
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .applet import *
|
||||
from .chat import *
|
||||
from .component import *
|
||||
from .session import *
|
||||
from .virtualapp import *
|
||||
|
||||
1
apps/terminal/api/chat/__init__.py
Normal file
1
apps/terminal/api/chat/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .chat import *
|
||||
15
apps/terminal/api/chat/chat.py
Normal file
15
apps/terminal/api/chat/chat.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from common.api import JMSBulkModelViewSet
|
||||
from terminal import serializers
|
||||
from terminal.filters import ChatFilter
|
||||
from terminal.models import Chat
|
||||
|
||||
__all__ = ['ChatViewSet']
|
||||
|
||||
|
||||
class ChatViewSet(JMSBulkModelViewSet):
|
||||
queryset = Chat.objects.all()
|
||||
serializer_class = serializers.ChatSerializer
|
||||
filterset_class = ChatFilter
|
||||
search_fields = ['title']
|
||||
ordering_fields = ['date_updated']
|
||||
ordering = ['-date_updated']
|
||||
@@ -2,7 +2,7 @@ from django.db.models import QuerySet
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from orgs.utils import filter_org_queryset
|
||||
from terminal.models import Command, CommandStorage, Session
|
||||
from terminal.models import Command, CommandStorage, Session, Chat
|
||||
|
||||
|
||||
class CommandFilter(filters.FilterSet):
|
||||
@@ -79,7 +79,34 @@ class CommandStorageFilter(filters.FilterSet):
|
||||
model = CommandStorage
|
||||
fields = ['real', 'name', 'type', 'is_default']
|
||||
|
||||
def filter_real(self, queryset, name, value):
|
||||
@staticmethod
|
||||
def filter_real(queryset, name, value):
|
||||
if value:
|
||||
queryset = queryset.exclude(name='null')
|
||||
return queryset
|
||||
|
||||
|
||||
class ChatFilter(filters.FilterSet):
|
||||
ids = filters.BooleanFilter(method='filter_ids')
|
||||
folder_ids = filters.BooleanFilter(method='filter_folder_ids')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Chat
|
||||
fields = [
|
||||
'title', 'user_id', 'pinned', 'folder_id',
|
||||
'archived', 'socket_id', 'share_id'
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def filter_ids(queryset, name, value):
|
||||
ids = value.split(',')
|
||||
queryset = queryset.filter(id__in=ids)
|
||||
return queryset
|
||||
|
||||
|
||||
@staticmethod
|
||||
def filter_folder_ids(queryset, name, value):
|
||||
ids = value.split(',')
|
||||
queryset = queryset.filter(folder_id__in=ids)
|
||||
return queryset
|
||||
|
||||
38
apps/terminal/migrations/0011_chat.py
Normal file
38
apps/terminal/migrations/0011_chat.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 4.1.13 on 2025-09-30 06:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('terminal', '0010_alter_command_risk_level_alter_session_login_from_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Chat',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=256, verbose_name='Title')),
|
||||
('chat', models.JSONField(default=dict, verbose_name='Chat')),
|
||||
('meta', models.JSONField(default=dict, verbose_name='Meta')),
|
||||
('pinned', models.BooleanField(default=False, verbose_name='Pinned')),
|
||||
('archived', models.BooleanField(default=False, verbose_name='Archived')),
|
||||
('share_id', models.CharField(blank=True, default='', max_length=36)),
|
||||
('folder_id', models.CharField(blank=True, default='', max_length=36)),
|
||||
('socket_id', models.CharField(blank=True, default='', max_length=36)),
|
||||
('user_id', models.CharField(blank=True, db_index=True, default='', max_length=36)),
|
||||
('session_info', models.JSONField(default=dict, verbose_name='Session Info')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Chat',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
from .session import *
|
||||
from .component import *
|
||||
from .applet import *
|
||||
from .chat import *
|
||||
from .component import *
|
||||
from .session import *
|
||||
from .virtualapp import *
|
||||
|
||||
1
apps/terminal/models/chat/__init__.py
Normal file
1
apps/terminal/models/chat/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .chat import *
|
||||
30
apps/terminal/models/chat/chat.py
Normal file
30
apps/terminal/models/chat/chat.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ['Chat']
|
||||
|
||||
|
||||
class Chat(JMSBaseModel):
|
||||
# id == session_id # 36 chars
|
||||
title = models.CharField(max_length=256, verbose_name=_('Title'))
|
||||
chat = models.JSONField(default=dict, verbose_name=_('Chat'))
|
||||
meta = models.JSONField(default=dict, verbose_name=_('Meta'))
|
||||
pinned = models.BooleanField(default=False, verbose_name=_('Pinned'))
|
||||
archived = models.BooleanField(default=False, verbose_name=_('Archived'))
|
||||
share_id = models.CharField(blank=True, default='', max_length=36)
|
||||
folder_id = models.CharField(blank=True, default='', max_length=36)
|
||||
socket_id = models.CharField(blank=True, default='', max_length=36)
|
||||
user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
|
||||
|
||||
session_info = models.JSONField(default=dict, verbose_name=_('Session Info'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Chat')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -123,11 +123,10 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
|
||||
def get_chat_ai_setting():
|
||||
data = get_chatai_data()
|
||||
return {
|
||||
'GPT_BASE_URL': data['url'],
|
||||
'GPT_API_KEY': data['api_key'],
|
||||
'GPT_PROXY': data['proxy'],
|
||||
'GPT_MODEL': data['model'],
|
||||
'CHAT_AI_TYPE': settings.CHAT_AI_TYPE,
|
||||
'GPT_BASE_URL': data.get('url'),
|
||||
'GPT_API_KEY': data.get('api_key'),
|
||||
'GPT_PROXY': data.get('proxy'),
|
||||
'CHAT_AI_PROVIDERS': settings.CHAT_AI_PROVIDERS,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
#
|
||||
from .applet import *
|
||||
from .applet_host import *
|
||||
from .chat import *
|
||||
from .command import *
|
||||
from .endpoint import *
|
||||
from .loki import *
|
||||
from .session import *
|
||||
from .sharing import *
|
||||
from .storage import *
|
||||
@@ -11,4 +13,3 @@ from .task import *
|
||||
from .terminal import *
|
||||
from .virtualapp import *
|
||||
from .virtualapp_provider import *
|
||||
from .loki import *
|
||||
|
||||
28
apps/terminal/serializers/chat.py
Normal file
28
apps/terminal/serializers/chat.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import CommonBulkModelSerializer
|
||||
from terminal.models import Chat
|
||||
|
||||
__all__ = ['ChatSerializer']
|
||||
|
||||
|
||||
class ChatSerializer(CommonBulkModelSerializer):
|
||||
created_at = serializers.SerializerMethodField()
|
||||
updated_at = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Chat
|
||||
fields_mini = ['id', 'title', 'created_at', 'updated_at']
|
||||
fields = fields_mini + [
|
||||
'chat', 'meta', 'pinned', 'archived',
|
||||
'share_id', 'folder_id',
|
||||
'user_id', 'session_info'
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_created_at(obj):
|
||||
return int(obj.date_created.timestamp())
|
||||
|
||||
@staticmethod
|
||||
def get_updated_at(obj):
|
||||
return int(obj.date_updated.timestamp())
|
||||
@@ -32,6 +32,7 @@ router.register(r'virtual-apps', api.VirtualAppViewSet, 'virtual-app')
|
||||
router.register(r'app-providers', api.AppProviderViewSet, 'app-provider')
|
||||
router.register(r'app-providers/((?P<provider>[^/.]+)/)?apps', api.AppProviderAppViewSet, 'app-provider-app')
|
||||
router.register(r'virtual-app-publications', api.VirtualAppPublicationViewSet, 'virtual-app-publication')
|
||||
router.register(r'chats', api.ChatViewSet, 'chat')
|
||||
|
||||
urlpatterns = [
|
||||
path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'),
|
||||
|
||||
@@ -199,11 +199,19 @@ class UserChangePasswordApi(UserQuerysetMixin, generics.UpdateAPIView):
|
||||
class UserUnblockPKApi(UserQuerysetMixin, generics.UpdateAPIView):
|
||||
serializer_class = serializers.UserSerializer
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
if is_uuid(pk):
|
||||
return super().get_object()
|
||||
else:
|
||||
return self.get_queryset().filter(username=pk).first()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
user = self.get_object()
|
||||
username = user.username if user else ''
|
||||
LoginBlockUtil.unblock_user(username)
|
||||
MFABlockUtils.unblock_user(username)
|
||||
if not user:
|
||||
return Response({"error": _("User not found")}, status=404)
|
||||
|
||||
user.unblock_login()
|
||||
|
||||
|
||||
class UserResetMFAApi(UserQuerysetMixin, generics.RetrieveAPIView):
|
||||
|
||||
@@ -274,8 +274,8 @@ class User(
|
||||
LoginBlockUtil.unblock_user(self.username)
|
||||
MFABlockUtils.unblock_user(self.username)
|
||||
|
||||
@lazyproperty
|
||||
def login_blocked(self):
|
||||
@property
|
||||
def is_login_blocked(self):
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
|
||||
if LoginBlockUtil.is_user_block(self.username):
|
||||
@@ -284,6 +284,13 @@ class User(
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def block_login(cls, username):
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
|
||||
LoginBlockUtil.block_user(username)
|
||||
MFABlockUtils.block_user(username)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.pk == 1 or self.username == "admin":
|
||||
raise PermissionDenied(_("Can not delete admin user"))
|
||||
|
||||
@@ -123,7 +123,7 @@ class UserSerializer(
|
||||
mfa_force_enabled = serializers.BooleanField(
|
||||
read_only=True, label=_("MFA force enabled")
|
||||
)
|
||||
login_blocked = serializers.BooleanField(read_only=True, label=_("Login blocked"))
|
||||
is_login_blocked = serializers.BooleanField(read_only=True, label=_("Login blocked"))
|
||||
is_expired = serializers.BooleanField(read_only=True, label=_("Is expired"))
|
||||
is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
|
||||
is_otp_secret_key_bound = serializers.BooleanField(
|
||||
@@ -193,6 +193,7 @@ class UserSerializer(
|
||||
"is_valid", "is_expired", "is_active", # 布尔字段
|
||||
"is_otp_secret_key_bound", "can_public_key_auth",
|
||||
"mfa_enabled", "need_update_password", "is_face_code_set",
|
||||
"is_login_blocked",
|
||||
]
|
||||
# 包含不太常用的字段,可以没有
|
||||
fields_verbose = (
|
||||
@@ -211,7 +212,7 @@ class UserSerializer(
|
||||
# 多对多字段
|
||||
fields_m2m = ["groups", "system_roles", "org_roles", "orgs_roles", "labels"]
|
||||
# 在serializer 上定义的字段
|
||||
fields_custom = ["login_blocked", "password_strategy"]
|
||||
fields_custom = ["is_login_blocked", "password_strategy"]
|
||||
fields = fields_verbose + fields_fk + fields_m2m + fields_custom
|
||||
fields_unexport = ["avatar_url", "is_service_account"]
|
||||
|
||||
|
||||
@@ -28,6 +28,6 @@ urlpatterns = [
|
||||
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),
|
||||
path('users/<uuid:pk>/password/reset/', api.UserResetPasswordApi.as_view(), name='user-reset-password'),
|
||||
path('users/<uuid:pk>/pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
|
||||
path('users/<uuid:pk>/unblock/', api.UserUnblockPKApi.as_view(), name='user-unblock'),
|
||||
path('users/<str:pk>/unblock/', api.UserUnblockPKApi.as_view(), name='user-unblock'),
|
||||
]
|
||||
urlpatterns += router.urls
|
||||
|
||||
@@ -186,6 +186,13 @@ class BlockUtilBase:
|
||||
def is_block(self):
|
||||
return bool(cache.get(self.block_key))
|
||||
|
||||
@classmethod
|
||||
def block_user(cls, username):
|
||||
username = username.lower()
|
||||
block_key = cls.BLOCK_KEY_TMPL.format(username)
|
||||
key_ttl = int(settings.SECURITY_LOGIN_LIMIT_TIME) * 60
|
||||
cache.set(block_key, True, key_ttl)
|
||||
|
||||
@classmethod
|
||||
def get_blocked_usernames(cls):
|
||||
key = cls.BLOCK_KEY_TMPL.format('*')
|
||||
|
||||
@@ -40,11 +40,11 @@ dependencies = [
|
||||
'pyyaml==6.0.1',
|
||||
'requests==2.32.4',
|
||||
'simplejson==3.19.1',
|
||||
'six==1.16.0',
|
||||
'six==1.17.0',
|
||||
'sshtunnel==0.4.0',
|
||||
'sshpubkeys==3.3.1',
|
||||
'uritemplate==4.1.1',
|
||||
'vine==5.0.0',
|
||||
'vine==5.1.0',
|
||||
'werkzeug==3.0.6',
|
||||
'unicodecsv==0.14.1',
|
||||
'httpsig==1.3.0',
|
||||
@@ -68,32 +68,32 @@ dependencies = [
|
||||
'ipip-ipdb==1.6.1',
|
||||
'pywinrm==0.4.3',
|
||||
'python-nmap==0.7.1',
|
||||
'django==4.1.13',
|
||||
'django==5.2.9',
|
||||
'django-bootstrap3==23.4',
|
||||
'django-filter==23.2',
|
||||
'django-filter==24.3',
|
||||
'django-formtools==2.5.1',
|
||||
'django-ranged-response==0.2.0',
|
||||
'django-simple-captcha==0.5.18',
|
||||
'django-timezone-field==5.1',
|
||||
'djangorestframework==3.14.0',
|
||||
'django-timezone-field==7.1',
|
||||
'djangorestframework==3.15.0',
|
||||
'djangorestframework-bulk==0.2.1',
|
||||
'django-simple-history==3.6.0',
|
||||
'django-private-storage==3.1',
|
||||
'django-private-storage==3.1.3',
|
||||
'drf-nested-routers==0.93.4',
|
||||
'drf-writable-nested==0.7.0',
|
||||
'rest-condition==1.0.3',
|
||||
'drf-spectacular==0.28.0',
|
||||
'pillow==10.2.0',
|
||||
'drf-spectacular==0.29.0',
|
||||
'pillow==12.0.0',
|
||||
'pytz==2025.2',
|
||||
'django-proxy==1.2.2',
|
||||
'python-daemon==3.0.1',
|
||||
'eventlet==0.40.3',
|
||||
'greenlet==3.2.4',
|
||||
'gunicorn==23.0.0',
|
||||
'celery==5.3.1',
|
||||
'celery==5.6.0',
|
||||
'flower==2.0.1',
|
||||
'django-celery-beat==2.6.0',
|
||||
'kombu==5.3.5',
|
||||
'django-celery-beat==2.8.1',
|
||||
'kombu==5.6.0',
|
||||
'uvicorn==0.22.0',
|
||||
'websockets==11.0.3',
|
||||
'python-ldap==3.4.5',
|
||||
@@ -103,10 +103,10 @@ dependencies = [
|
||||
'python-cas==1.6.0',
|
||||
'django-auth-ldap==4.4.0',
|
||||
'mysqlclient==2.2.4',
|
||||
'pymssql==2.3.4',
|
||||
'django-redis==5.3.0',
|
||||
'pymssql==2.3.10',
|
||||
'django-redis==5.4.0',
|
||||
'python-redis-lock==4.0.0',
|
||||
'pyopenssl==23.2.0',
|
||||
'pyopenssl==24.0.0',
|
||||
'redis',
|
||||
'pymongo==4.6.3',
|
||||
'pyfreerdp==0.0.2',
|
||||
@@ -153,6 +153,7 @@ dependencies = [
|
||||
'pdf2image==1.17.0',
|
||||
'drf-spectacular-sidecar==2025.8.1',
|
||||
"django-oauth-toolkit==2.4.0",
|
||||
"telnetlib3==2.0.8",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -19,3 +19,10 @@ fi
|
||||
echo "4. For Apple processor"
|
||||
LDFLAGS="-L$(brew --prefix freetds)/lib -L$(brew --prefix openssl@1.1)/lib" CFLAGS="-I$(brew --prefix freetds)/include" pip install $(grep 'pymssql' requirements.txt)
|
||||
export PKG_CONFIG_PATH="/opt/homebrew/opt/mysql-client/lib/pkgconfig"
|
||||
|
||||
|
||||
echo "5. Install Ansible Receptor"
|
||||
export RECEPTOR_VERSION=v1.4.5
|
||||
export ARCH=`arch`
|
||||
wget -O ${TMPDIR}receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_darwin_${ARCH}.tar.gz
|
||||
tar -xf ${TMPDIR}receptor.tar.gz -C /opt/homebrew/bin/
|
||||
Reference in New Issue
Block a user