mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-24 13:02:37 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d37dca0de | ||
|
|
8730fa8dee | ||
|
|
9a5a775652 | ||
|
|
8304ae9070 | ||
|
|
9533861e24 | ||
|
|
abbfbcde83 | ||
|
|
046a9d41bf | ||
|
|
363bb20da7 | ||
|
|
2b7c8b9c07 | ||
|
|
db040bbd06 | ||
|
|
a761ec9aa1 | ||
|
|
c0ffe45ce9 | ||
|
|
404d58a9c9 | ||
|
|
f64eab7a15 | ||
|
|
46f94fd138 | ||
|
|
2f1c0090b7 | ||
|
|
b0d6a09276 | ||
|
|
d8db76cc7b | ||
|
|
b35a55ed54 | ||
|
|
dc5ecfcc4b | ||
|
|
2ca4002624 | ||
|
|
543dde57ab | ||
|
|
c088437fe5 | ||
|
|
e721ec147c | ||
|
|
5d18d6dee0 | ||
|
|
ecfd338428 | ||
|
|
4b28b079dc | ||
|
|
c1c3236a30 | ||
|
|
4b19750581 | ||
|
|
eafb5ecfb3 | ||
|
|
583486e26e | ||
|
|
8198620a2e | ||
|
|
c0b301d52b | ||
|
|
7791d6222a | ||
|
|
b740d9d42f | ||
|
|
48d0187604 | ||
|
|
6217018427 | ||
|
|
923f40e523 | ||
|
|
1f1fe2084b | ||
|
|
b8b1a6ac9c | ||
|
|
35f88722af | ||
|
|
7e6d2749ae | ||
|
|
be57b101ff | ||
|
|
41c8cb6307 | ||
|
|
3a7ae01ede |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,3 +45,4 @@ test.py
|
||||
.history/
|
||||
.test/
|
||||
*.mo
|
||||
apps.iml
|
||||
|
||||
@@ -89,7 +89,7 @@ JumpServer is a mission critical product. Please refer to the Basic Security Rec
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2024 FIT2CLOUD, All rights reserved.
|
||||
Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
@@ -70,6 +70,13 @@ class ActionAclSerializer(serializers.Serializer):
|
||||
return
|
||||
if not settings.XPACK_LICENSE_IS_VALID:
|
||||
field_action._choices.pop(ActionChoices.review, None)
|
||||
if not (
|
||||
settings.XPACK_LICENSE_IS_VALID and
|
||||
settings.XPACK_LICENSE_EDITION_ULTIMATE and
|
||||
settings.FACE_RECOGNITION_ENABLED
|
||||
):
|
||||
field_action._choices.pop(ActionChoices.face_verify, None)
|
||||
field_action._choices.pop(ActionChoices.face_online, None)
|
||||
for choice in self.Meta.action_choices_exclude:
|
||||
field_action._choices.pop(choice, None)
|
||||
|
||||
|
||||
@@ -123,13 +123,15 @@ class RDPFileClientProtocolURLMixin:
|
||||
# rdp_options['domain:s'] = token.account_ad_domain
|
||||
|
||||
# 设置宽高
|
||||
height = self.request.query_params.get('height')
|
||||
width = self.request.query_params.get('width')
|
||||
if width and height:
|
||||
rdp_options['desktopwidth:i'] = width
|
||||
rdp_options['desktopheight:i'] = height
|
||||
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
|
||||
rdp_options['dynamic resolution:i'] = '0'
|
||||
|
||||
resolution_value = token.connect_options.get('resolution', 'auto')
|
||||
if resolution_value != 'auto':
|
||||
width, height = resolution_value.split('x')
|
||||
if width and height:
|
||||
rdp_options['desktopwidth:i'] = width
|
||||
rdp_options['desktopheight:i'] = height
|
||||
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
|
||||
rdp_options['dynamic resolution:i'] = '0'
|
||||
|
||||
color_quality = self.request.query_params.get('rdp_color_quality')
|
||||
color_quality = color_quality if color_quality else os.getenv('JUMPSERVER_COLOR_DEPTH', RDPColorQuality.HIGH)
|
||||
|
||||
@@ -30,10 +30,11 @@ class MFAFace(BaseMFA, AuthFaceMixin):
|
||||
|
||||
@staticmethod
|
||||
def global_enabled():
|
||||
return settings.XPACK_LICENSE_IS_VALID \
|
||||
and LicenseEditionChoices.ULTIMATE == \
|
||||
LicenseEditionChoices.from_key(settings.XPACK_LICENSE_EDITION) \
|
||||
and settings.FACE_RECOGNITION_ENABLED
|
||||
return (
|
||||
settings.XPACK_LICENSE_IS_VALID and
|
||||
settings.XPACK_LICENSE_EDITION_ULTIMATE and
|
||||
settings.FACE_RECOGNITION_ENABLED
|
||||
)
|
||||
|
||||
def get_enable_url(self) -> str:
|
||||
return '/ui/#/profile/index'
|
||||
|
||||
@@ -51,7 +51,7 @@ auth._get_backends = _get_backends
|
||||
def authenticate(request=None, **credentials):
|
||||
"""
|
||||
If the given credentials are valid, return a User object.
|
||||
之所以 hack 这个 auticate
|
||||
之所以 hack 这个 authenticate
|
||||
"""
|
||||
username = credentials.get('username')
|
||||
|
||||
@@ -500,10 +500,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, AuthFaceMixin, MFA
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
|
||||
def _check_auth_user_is_valid(self, username, password, public_key):
|
||||
user = authenticate(
|
||||
self.request, username=username,
|
||||
password=password, public_key=public_key
|
||||
)
|
||||
credentials = {'username': username}
|
||||
if password:
|
||||
credentials['password'] = password
|
||||
if public_key:
|
||||
credentials['public_key'] = public_key
|
||||
user = authenticate(self.request, **credentials)
|
||||
if not user:
|
||||
self.raise_credential_error(errors.reason_password_failed)
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
if 'next=client' in redirect_url:
|
||||
if redirect_url and 'next=client' in redirect_url:
|
||||
self.request.META['QUERY_STRING'] += '&next=client'
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, empty
|
||||
|
||||
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
|
||||
from common.utils import decrypt_password
|
||||
from common.utils import decrypt_password, is_uuid
|
||||
|
||||
__all__ = [
|
||||
"ReadableHiddenField",
|
||||
@@ -127,12 +127,14 @@ class LabelRelatedField(serializers.RelatedField):
|
||||
if isinstance(data, dict) and (data.get("id") or data.get("pk")):
|
||||
pk = data.get("id") or data.get("pk")
|
||||
label = Label.objects.get(pk=pk)
|
||||
elif is_uuid(data):
|
||||
label = Label.objects.get(pk=data)
|
||||
else:
|
||||
if isinstance(data, dict):
|
||||
k = data.get("name")
|
||||
v = data.get("value")
|
||||
elif isinstance(data, str) and ":" in data:
|
||||
k, v = data.split(":", 1)
|
||||
k, v = [x.strip() for x in data.split(":", 1)]
|
||||
else:
|
||||
raise serializers.ValidationError(_("Invalid data type"))
|
||||
label, __ = Label.objects.get_or_create(name=k, value=v, defaults={'name': k, 'value': v})
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"ConnectError": "Error while fetching data",
|
||||
"ConnectSuccess": "Connected successfully",
|
||||
"Connected": "Connected",
|
||||
"Copy": "Copy",
|
||||
"CopyFailed": "Copy failed",
|
||||
"CopyNotAllowed": "You are not allowed to copy, please contact the administrator to open it!",
|
||||
"CopySucceeded": "Copy succeeded",
|
||||
"Current": "Current",
|
||||
"DatabaseExplorer": "Database Explorer",
|
||||
"DatabaseProperties": "Datasource properties",
|
||||
@@ -33,6 +36,7 @@
|
||||
"InitializeDatasource": "Initialize datasource",
|
||||
"InitializeDatasourceFailed": "Initialize datasource failed",
|
||||
"InitializingDatasourceMessage": "Initializing data source, please wait...",
|
||||
"InsertStatement": "Insert statement",
|
||||
"JDBCURL": "JDBC URL",
|
||||
"LogOutput": "Log Output",
|
||||
"Name": "Name",
|
||||
@@ -67,11 +71,14 @@
|
||||
"Submit": "Submit",
|
||||
"Total": "Total",
|
||||
"Type": "Type",
|
||||
"UpdateStatement": "Update statement",
|
||||
"User": "User",
|
||||
"UserCancelCommandReviewError": "The user has cancelled the command review.",
|
||||
"Version": "Version",
|
||||
"ViewData": "View data",
|
||||
"WaitCommandReviewMessage": "The review request has been initiated, please wait for the review results",
|
||||
"Warning": "Warning",
|
||||
"initializingDatasourceFailedMessage": "Connection failed, please check if the database connection configuration is correct"
|
||||
}
|
||||
"initializingDatasourceFailedMessage": "Connection failed, please check if the database connection configuration is correct",
|
||||
"Scope": "Scope",
|
||||
"Format": "Format"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"ConnectError": "接続に失敗しました",
|
||||
"ConnectSuccess": "接続に成功しました",
|
||||
"Connected": "接続済み",
|
||||
"Copy": "コピー",
|
||||
"CopyFailed": "コピーしっぱい",
|
||||
"CopyNotAllowed": "コピーは許可されていません。管理者に連絡して権限を開いてください!",
|
||||
"CopySucceeded": "コピーせいこう",
|
||||
"Current": "現在",
|
||||
"DatabaseExplorer": "データベースエクスプローラー",
|
||||
"DatabaseProperties": "データソースプロパティ",
|
||||
@@ -29,10 +32,12 @@
|
||||
"ExportCurrent": "現在のページをエクスポート",
|
||||
"ExportData": "データをエクスポート",
|
||||
"FetchError": "データの取得に失敗しました",
|
||||
"Format": "フォーマット",
|
||||
"FormatHotKey": "フォーマット (Ctrl + L)",
|
||||
"InitializeDatasource": "データソースを初期化",
|
||||
"InitializeDatasourceFailed": "データソースの初期化に失敗しました",
|
||||
"InitializingDatasourceMessage": "データソースを初期化中、しばらくお待ちください...",
|
||||
"InsertStatement": "インサート文",
|
||||
"JDBCURL": "JDBC URL",
|
||||
"LogOutput": "ログ出力",
|
||||
"Name": "名前",
|
||||
@@ -56,6 +61,7 @@
|
||||
"Save": "保存",
|
||||
"SaveSQL": "SQLを保存",
|
||||
"SaveSucceed": "保存に成功しました",
|
||||
"Scope": "範囲",
|
||||
"SelectSQL": "SQLを選択",
|
||||
"SessionClosedBy": "セッションが%sによって閉じられました",
|
||||
"SessionFinished": "セッションが終了しました",
|
||||
@@ -67,6 +73,7 @@
|
||||
"Submit": "提出",
|
||||
"Total": "合計",
|
||||
"Type": "タイプ",
|
||||
"UpdateStatement": "アップデート文",
|
||||
"User": "ユーザー",
|
||||
"UserCancelCommandReviewError": "ユーザーがコマンドレビューをキャンセルしました",
|
||||
"Version": "バージョン",
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"ConnectError": "Falha na conexão",
|
||||
"ConnectSuccess": "Conectado com sucesso",
|
||||
"Connected": "Conectado",
|
||||
"Copy": "cópia",
|
||||
"CopyFailed": "Cópia falhada",
|
||||
"CopyNotAllowed": "Cópia não permitida, por favor contate o administrador para liberar a permissão!",
|
||||
"CopySucceeded": "Cópia bem-sucedida",
|
||||
"Current": "Atual",
|
||||
"DatabaseExplorer": "Navegador de Banco de Dados",
|
||||
"DatabaseProperties": "Propriedades da fonte de dados",
|
||||
@@ -29,10 +32,12 @@
|
||||
"ExportCurrent": "Exportar página atual",
|
||||
"ExportData": "Exportar dados",
|
||||
"FetchError": "Falha ao obter os dados",
|
||||
"Format": "Formato ",
|
||||
"FormatHotKey": "Formatar (Ctrl + L)",
|
||||
"InitializeDatasource": "Inicializar fonte de dados",
|
||||
"InitializeDatasourceFailed": "Falha ao inicializar a fonte de dados",
|
||||
"InitializingDatasourceMessage": "Inicializando a fonte de dados, por favor aguarde...",
|
||||
"InsertStatement": "instrução de inserção",
|
||||
"JDBCURL": "URL JDBC",
|
||||
"LogOutput": "Log de saída",
|
||||
"Name": "Nome",
|
||||
@@ -56,6 +61,7 @@
|
||||
"Save": "Salvar",
|
||||
"SaveSQL": "Salve o SQL",
|
||||
"SaveSucceed": "Salvo com sucesso",
|
||||
"Scope": " Alcance",
|
||||
"SelectSQL": "Selecionar SQL",
|
||||
"SessionClosedBy": "A sessão foi fechada por %s",
|
||||
"SessionFinished": "A sessão terminou",
|
||||
@@ -67,6 +73,7 @@
|
||||
"Submit": "Submeter",
|
||||
"Total": "Total",
|
||||
"Type": "Escrever",
|
||||
"UpdateStatement": "instrução de atualização",
|
||||
"User": "Usuário",
|
||||
"UserCancelCommandReviewError": "O usuário cancelou a revisão do comando",
|
||||
"Version": "Versão",
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"ConnectError": "连接失败",
|
||||
"ConnectSuccess": "连接成功",
|
||||
"Connected": "已连接",
|
||||
"Copy": "复制",
|
||||
"CopyFailed": "复制失败",
|
||||
"CopyNotAllowed": "不允许复制,请联系管理员开启权限!",
|
||||
"CopySucceeded": "复制成功",
|
||||
"Current": "当前",
|
||||
"DatabaseExplorer": "数据库浏览器",
|
||||
"DatabaseProperties": "数据源属性",
|
||||
@@ -33,6 +36,7 @@
|
||||
"InitializeDatasource": "初始化数据源",
|
||||
"InitializeDatasourceFailed": "初始化数据源失败",
|
||||
"InitializingDatasourceMessage": "正在初始化数据源,请稍候...",
|
||||
"InsertStatement": "插入语句",
|
||||
"JDBCURL": "JDBC URL",
|
||||
"LogOutput": "日志输出",
|
||||
"Name": "名称",
|
||||
@@ -67,11 +71,14 @@
|
||||
"Submit": "提交",
|
||||
"Total": "总计",
|
||||
"Type": "类型",
|
||||
"UpdateStatement": "更新语句",
|
||||
"User": "用户",
|
||||
"UserCancelCommandReviewError": "用户取消命令复核",
|
||||
"Version": "版本",
|
||||
"ViewData": "查看数据",
|
||||
"WaitCommandReviewMessage": "复核请求已发起, 请等待复核结果",
|
||||
"Warning": "警告",
|
||||
"initializingDatasourceFailedMessage": "连接失败,请检查数据库连接配置是否正确"
|
||||
}
|
||||
"initializingDatasourceFailedMessage": "连接失败,请检查数据库连接配置是否正确",
|
||||
"Scope": "范围",
|
||||
"Format": "格式"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"ConnectError": "連接失敗",
|
||||
"ConnectSuccess": "連接成功",
|
||||
"Connected": "已連接",
|
||||
"Copy": "複製",
|
||||
"CopyFailed": "複製失敗",
|
||||
"CopyNotAllowed": "不允許複製,請聯絡管理員開啟權限!",
|
||||
"CopySucceeded": "複製成功",
|
||||
"Current": "目前",
|
||||
"DatabaseExplorer": "資料庫瀏覽器",
|
||||
"DatabaseProperties": "資料源屬性",
|
||||
@@ -29,10 +32,12 @@
|
||||
"ExportCurrent": "匯出當前頁面",
|
||||
"ExportData": "匯出資料",
|
||||
"FetchError": "獲取資料失敗",
|
||||
"Format": "格式",
|
||||
"FormatHotKey": "格式化 (Ctrl + L)",
|
||||
"InitializeDatasource": "初始化資料源",
|
||||
"InitializeDatasourceFailed": "初始化資料源失敗",
|
||||
"InitializingDatasourceMessage": "正在初始化資料源,請稍候...",
|
||||
"InsertStatement": "插入語句",
|
||||
"JDBCURL": "JDBC URL",
|
||||
"LogOutput": "日誌輸出",
|
||||
"Name": "名稱",
|
||||
@@ -56,6 +61,7 @@
|
||||
"Save": "保存",
|
||||
"SaveSQL": "儲存 SQL",
|
||||
"SaveSucceed": "儲存成功",
|
||||
"Scope": "範圍",
|
||||
"SelectSQL": "選擇 SQL",
|
||||
"SessionClosedBy": "會話被 %s 關閉",
|
||||
"SessionFinished": "會話已結束",
|
||||
@@ -67,6 +73,7 @@
|
||||
"Submit": "提交",
|
||||
"Total": "總計",
|
||||
"Type": "類型",
|
||||
"UpdateStatement": "更新語句",
|
||||
"User": "用戶",
|
||||
"UserCancelCommandReviewError": "用戶取消命令覆核",
|
||||
"Version": "版本",
|
||||
|
||||
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
@@ -98,7 +98,7 @@
|
||||
"AppletHelpText": "In the upload process, if the application does not exist, create the application; if it exists, update the application.",
|
||||
"AppletHostCreate": "Add RemoteApp machine",
|
||||
"AppletHostDetail": "RemoteApp machine",
|
||||
"AppletHostSelectHelpMessage": "When connecting to an asset, the selection of the application publishing machine is random (but the last used one is preferred). if you want to assign a specific publishing machine to an asset, you can tag it as <publishing machine: publishing machine name> or <applethost: publishing machine name>; <br>when selecting an account for the publishing machine, the following situations will choose the user's own <b>account with the same name or proprietary account (starting with js)</b>, otherwise use a public account (starting with jms):<br> 1. both the publishing machine and application support concurrent;<br> 2. the publishing machine supports concurrent, but the application does not, and the current application does not use a proprietary account;<br> 3. the publishing machine does not support concurrent, the application either supports or does not support concurrent, and no application uses a proprietary account;<br> note: whether the application supports concurrent connections is decided by the developer, and whether the host supports concurrent connections is decided by the single user single session setting in the publishing machine configuration",
|
||||
"AppletHostSelectHelpMessage": "When connecting to an asset, the selection of the application publishing machine is random (but the last used one is preferred). if you want to assign a specific publishing machine to an asset, you can tag it as [publishing machine: publishing machine name] or [AppletHost: publishing machine name]; <br>when selecting an account for the publishing machine, the following situations will choose the user's own <b>account with the same name or proprietary account (starting with js)</b>, otherwise use a public account (starting with jms):<br> 1. both the publishing machine and application support concurrent;<br> 2. the publishing machine supports concurrent, but the application does not, and the current application does not use a proprietary account;<br> 3. the publishing machine does not support concurrent, the application either supports or does not support concurrent, and no application uses a proprietary account;<br> note: whether the application supports concurrent connections is decided by the developer, and whether the host supports concurrent connections is decided by the single user single session setting in the publishing machine configuration",
|
||||
"AppletHostUpdate": "Update the remote app publishing machine",
|
||||
"AppletHostZoneHelpText": "This domain belongs to the system organization",
|
||||
"AppletHosts": "RemoteApp machine",
|
||||
@@ -462,6 +462,7 @@
|
||||
"Edit": "Edit",
|
||||
"EditRecipient": "Edit recipient",
|
||||
"Edition": "Version",
|
||||
"Effective": "Effective",
|
||||
"Email": "Email",
|
||||
"EmailContent": "Custom content",
|
||||
"EmailTemplate": "Template",
|
||||
@@ -1422,5 +1423,8 @@
|
||||
"disallowSelfUpdateFields": "Not allowed to modify the current fields yourself",
|
||||
"forceEnableMFAHelpText": "If force enable, user can not disable by themselves",
|
||||
"removeWarningMsg": "Are you sure you want to remove",
|
||||
"setVariable": "Set variable"
|
||||
"setVariable": "Set variable",
|
||||
"VerifyFace": "Verify face",
|
||||
"ServerBusyRetry": "The server is busy, please try again later.",
|
||||
"DeeplyThoughtAbout": "Deeply thought about"
|
||||
}
|
||||
@@ -432,6 +432,7 @@
|
||||
"Datetime": "日時",
|
||||
"Day": "日",
|
||||
"DeclassificationLogNum": "パスワード変更ログ数",
|
||||
"DeeplyThoughtAbout": "深く考えた結果",
|
||||
"DefaultDatabase": "デフォルトのデータベース",
|
||||
"DefaultPort": "デフォルトポート",
|
||||
"DefaultValue": "デフォルト値",
|
||||
@@ -478,6 +479,7 @@
|
||||
"Edit": "編集",
|
||||
"EditRecipient": "受取人の編集",
|
||||
"Edition": "バージョン",
|
||||
"Effective": "効果",
|
||||
"Email": "メールボックス",
|
||||
"EmailContent": "メールコンテンツのカスタマイズ",
|
||||
"EmailTemplate": "メールテンプレート",
|
||||
@@ -1139,6 +1141,7 @@
|
||||
"SerialNumber": "シリアルナンバー",
|
||||
"Server": "サービス",
|
||||
"ServerAccountKey": "サービスアカウントキー",
|
||||
"ServerBusyRetry": "サーバーが混雑していますので、後ほど再度お試しください。",
|
||||
"ServerError": "サーバーエラー",
|
||||
"ServerTime": "サーバータイム",
|
||||
"Session": "コンバセーション",
|
||||
@@ -1420,6 +1423,7 @@
|
||||
"VaultHCPMountPoint": "Vault サーバのマウントポイント、デフォルトはjumpserver",
|
||||
"VaultHelpText": "1. セキュリティ上の理由から、設定ファイルに VAULT_ENABLED=true および VAULT_BACKEND=hcp/azure/aws のパラメータを追加して、Vault ストレージを有効にします。<br>2. オンにした後、他の設定を入力してテストを行います。<br>3. データ同期を行います。同期は一方向です。ローカルデータベースからリモートの Vault にのみ同期します。同期が終了すればローカルデータベースはパスワードを保管していませんので、データのバックアップをお願いします。<br>4. Vault の設定を二度変更した後はサービスを再起動する必要があります。",
|
||||
"VerificationCodeSent": "認証コードが送信されました",
|
||||
"VerifyFace": "顔認証を行います",
|
||||
"VerifySignTmpl": "認証コードのSMSテンプレート",
|
||||
"Version": "バージョン",
|
||||
"View": "閲覧",
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
"Datetime": "Data e Hora",
|
||||
"Day": "Dia",
|
||||
"DeclassificationLogNum": "Quantidade de logs de modificação de senha",
|
||||
"DeeplyThoughtAbout": "Já refletido profundamente ",
|
||||
"DefaultDatabase": "Banco de Dados Padrão",
|
||||
"DefaultPort": "Porta padrão",
|
||||
"DefaultValue": " Valor padrão",
|
||||
@@ -463,6 +464,7 @@
|
||||
"Edit": "Editar",
|
||||
"EditRecipient": "Editar receptor",
|
||||
"Edition": "Versão",
|
||||
"Effective": "Eficaz",
|
||||
"Email": "Email",
|
||||
"EmailContent": "Personalização de conteúdo de e-mail",
|
||||
"EmailTemplate": " Modelo de e-mail",
|
||||
@@ -1107,6 +1109,7 @@
|
||||
"SerialNumber": "Número de Série",
|
||||
"Server": "Serviços",
|
||||
"ServerAccountKey": "Chave da conta de serviço",
|
||||
"ServerBusyRetry": " O servidor está ocupado, por favor, tente novamente mais tarde.",
|
||||
"ServerError": "Erro do servidor",
|
||||
"ServerTime": "Horário do Servidor",
|
||||
"Session": "Sessão",
|
||||
@@ -1382,6 +1385,7 @@
|
||||
"VaultHCPMountPoint": "Ponto de montagem do servidor Vault, padrão é jumpserver",
|
||||
"VaultHelpText": "1. Por razões de segurança, é necessário adicionar os parâmetros VAULT_ENABLED=true e VAULT_BACKEND=hcp/azure/aws no arquivo de configuração para ativar o armazenamento Vault.<br>2. Depois de ativado, preencha as outras configurações e faça o teste.<br>3. Proceda com a sincronização de dados, a sincronização é unidirecional, sincronizará apenas do banco de dados local para o Vault remoto, após a conclusão da sincronização, o banco de dados local não armazenará mais a senha, por favor, faça backup dos dados.<br>4. Após a segunda modificação da configuração do Vault, é necessário reiniciar o serviço.",
|
||||
"VerificationCodeSent": "O código de verificação foi enviado",
|
||||
"VerifyFace": " Verificar rosto ",
|
||||
"VerifySignTmpl": "Modelo de SMS com código de verificação",
|
||||
"Version": "Versão",
|
||||
"View": " Visualizar",
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"AppletHelpText": "在上传过程中,如果应用不存在,则创建该应用;如果已存在,则进行应用更新。",
|
||||
"AppletHostCreate": "添加远程应用发布机",
|
||||
"AppletHostDetail": "远程应用发布机详情",
|
||||
"AppletHostSelectHelpMessage": "连接资产时,应用发布机选择是随机的(但优先选择上次使用的),如果想为某个资产固定发布机,可以指定标签 <发布机:发布机名称> 或 <AppletHost:发布机名称>; <br>连接该发布机选择账号时,以下情况会选择用户的 <b>同名账号 或 专有账号(js开头)</b>,否则使用公用账号(jms开头):<br> 1. 发布机和应用都支持并发; <br> 2. 发布机支持并发,应用不支持并发,当前应用没有使用专有账号; <br> 3. 发布机不支持并发,应用支持并发或不支持,没有任一应用使用专有账号; <br> 注意: 应用支不支持并发是开发者决定,主机支不支持是发布机配置中的 单用户单会话决定",
|
||||
"AppletHostSelectHelpMessage": "连接资产时,应用发布机选择是随机的(但优先选择上次使用的),如果想为某个资产固定发布机,可以指定标签 [发布机:发布机名称] 或 [AppletHost:发布机名称]; <br>连接该发布机选择账号时,以下情况会选择用户的 <b>同名账号 或 专有账号(js开头)</b>,否则使用公用账号(jms开头):<br> 1. 发布机和应用都支持并发; <br> 2. 发布机支持并发,应用不支持并发,当前应用没有使用专有账号; <br> 3. 发布机不支持并发,应用支持并发或不支持,没有任一应用使用专有账号; <br> 注意: 应用支不支持并发是开发者决定,主机支不支持是发布机配置中的 单用户单会话决定",
|
||||
"AppletHostUpdate": "更新远程应用发布机",
|
||||
"AppletHostZoneHelpText": "这里的网域属于 System 组织",
|
||||
"AppletHosts": "应用发布机",
|
||||
@@ -463,6 +463,7 @@
|
||||
"Edit": "编辑",
|
||||
"EditRecipient": "编辑接收人",
|
||||
"Edition": "版本",
|
||||
"Effective": "生效",
|
||||
"Email": "邮箱",
|
||||
"EmailContent": "邮件内容定制",
|
||||
"EmailTemplate": "邮件模版",
|
||||
@@ -1427,5 +1428,8 @@
|
||||
"disallowSelfUpdateFields": "不允许自己修改当前字段",
|
||||
"forceEnableMFAHelpText": "如果强制启用,用户无法自行禁用",
|
||||
"removeWarningMsg": "你确定要移除",
|
||||
"setVariable": "设置参数"
|
||||
"setVariable": "设置参数",
|
||||
"VerifyFace": "验证人脸",
|
||||
"ServerBusyRetry": "服务器繁忙,请稍后再试。",
|
||||
"DeeplyThoughtAbout": "已深度思考"
|
||||
}
|
||||
@@ -550,6 +550,7 @@
|
||||
"Day": "日",
|
||||
"DeactiveSelected": "禁用所選",
|
||||
"DeclassificationLogNum": "改密日誌數",
|
||||
"DeeplyThoughtAbout": "已深度思考",
|
||||
"Default": "預設的",
|
||||
"DefaultDatabase": "默認資料庫",
|
||||
"DefaultPort": "默認埠",
|
||||
@@ -614,6 +615,7 @@
|
||||
"Edit": "編輯",
|
||||
"EditRecipient": "編輯接收人",
|
||||
"Edition": "版本",
|
||||
"Effective": "生效",
|
||||
"Email": "信箱",
|
||||
"EmailContent": "郵件內容訂製",
|
||||
"EmailTemplate": " Email Template",
|
||||
@@ -1455,6 +1457,7 @@
|
||||
"SerialNumber": "序號",
|
||||
"Server": "服務",
|
||||
"ServerAccountKey": "服務帳號金鑰",
|
||||
"ServerBusyRetry": "伺服器繁忙,請稍後再試。",
|
||||
"ServerError": "伺服器錯誤",
|
||||
"ServerTime": "伺服器時間",
|
||||
"ServiceRatio": "組件負載統計",
|
||||
@@ -1815,6 +1818,7 @@
|
||||
"VaultHelpText": "1. 由於安全原因,需要在配置文件中增加參數 VAULT_ENABLED=true 和 VAULT_BACKEND=hcp/azure/aws 以開啟 Vault 存儲。<br>2. 開啟後,填寫其他配置,進行測試。<br>3. 進行數據同步,同步是單向的,只會從本地資料庫同步到遠端 Vault,同步完成本地資料庫不再儲存密碼,請備份好數據。<br>4. 二次修改 Vault 配置後需重啟服務。",
|
||||
"Vendor": "製造商",
|
||||
"VerificationCodeSent": "驗證碼已發送",
|
||||
"VerifyFace": "驗證面孔",
|
||||
"VerifySignTmpl": "驗證碼簡訊模板",
|
||||
"Version": "版本",
|
||||
"View": "查看",
|
||||
|
||||
@@ -232,5 +232,6 @@
|
||||
"start time": "start time",
|
||||
"success": "success",
|
||||
"system user": "system user",
|
||||
"user": "user"
|
||||
"user": "user",
|
||||
"SessionIsBeingMonitored": "session is being monitored"
|
||||
}
|
||||
@@ -71,6 +71,9 @@
|
||||
"Expand all": "Expandir Tudo",
|
||||
"Expand all asset": "Expanda todos os ativos abaixo do nó",
|
||||
"Expire time": "Tempo de Expiração",
|
||||
"Face online required": "Este login precisa de verificação facial e monitoramento, deseja continuar?",
|
||||
"Face verify required": "Este login requer verificação facial, deseja continuar? ",
|
||||
"Face verify success": "Verificação facial bem-sucedida. ",
|
||||
"Failed to open address": "Falha ao abrir o endereço",
|
||||
"Favorite": "Favoritos",
|
||||
"File Manager": "Arquivo gerenciar",
|
||||
@@ -121,6 +124,7 @@
|
||||
"No": "Não",
|
||||
"No account available": "Não há contas disponíveis",
|
||||
"No available connect method": "Nenhum método de conexão disponível",
|
||||
"No facial features": "Não há características faciais disponíveis no momento, por favor, dirija-se à página de informações pessoais para vincular. ",
|
||||
"No matching found": "Sem correspondências",
|
||||
"No permission": "Sem permissão",
|
||||
"No protocol available": "Não há protocolos disponíveis",
|
||||
@@ -168,7 +172,7 @@
|
||||
"Send text to all ssh terminals": "Enviar texto para todos os terminais ssh",
|
||||
"Set reusable": "Iniciar reutilização",
|
||||
"Setting": "Configurações",
|
||||
"Settings or basic settings": "Menu 设置 → Configurações básicas",
|
||||
"Settings or basic settings": "Menu configurar → Configurações básicas",
|
||||
"ShareSession": "Compartilhamento de sessão",
|
||||
"Show left manager": "Mostrar Barra Lateral Esquerda",
|
||||
"Skip": "Pular",
|
||||
|
||||
@@ -230,5 +230,6 @@
|
||||
"start time": "开始时间",
|
||||
"success": "成功",
|
||||
"system user": "系统用户",
|
||||
"user": "用户"
|
||||
"user": "用户",
|
||||
"SessionIsBeingMonitored": "会话正在被监控"
|
||||
}
|
||||
@@ -290,7 +290,7 @@ class Config(dict):
|
||||
'AUTH_LDAP_START_TLS': False,
|
||||
'AUTH_LDAP_USER_ATTR_MAP': {"username": "cn", "name": "sn", "email": "mail"},
|
||||
'AUTH_LDAP_CONNECT_TIMEOUT': 10,
|
||||
'AUTH_LDAP_CACHE_TIMEOUT': 3600 * 24 * 30,
|
||||
'AUTH_LDAP_CACHE_TIMEOUT': 0,
|
||||
'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000,
|
||||
'AUTH_LDAP_SYNC_IS_PERIODIC': False,
|
||||
'AUTH_LDAP_SYNC_INTERVAL': None,
|
||||
@@ -310,7 +310,7 @@ class Config(dict):
|
||||
'AUTH_LDAP_HA_START_TLS': False,
|
||||
'AUTH_LDAP_HA_USER_ATTR_MAP': {"username": "cn", "name": "sn", "email": "mail"},
|
||||
'AUTH_LDAP_HA_CONNECT_TIMEOUT': 10,
|
||||
'AUTH_LDAP_HA_CACHE_TIMEOUT': 3600 * 24 * 30,
|
||||
'AUTH_LDAP_HA_CACHE_TIMEOUT': 0,
|
||||
'AUTH_LDAP_HA_SEARCH_PAGED_SIZE': 1000,
|
||||
'AUTH_LDAP_HA_SYNC_IS_PERIODIC': False,
|
||||
'AUTH_LDAP_HA_SYNC_INTERVAL': None,
|
||||
@@ -681,10 +681,15 @@ class Config(dict):
|
||||
|
||||
# Chat AI
|
||||
'CHAT_AI_ENABLED': False,
|
||||
'GPT_API_KEY': '',
|
||||
'CHAT_AI_TYPE': 'gpt',
|
||||
'GPT_BASE_URL': '',
|
||||
'GPT_API_KEY': '',
|
||||
'GPT_PROXY': '',
|
||||
'GPT_MODEL': 'gpt-3.5-turbo',
|
||||
'GPT_MODEL': 'gpt-4o-mini',
|
||||
'DEEPSEEK_BASE_URL': '',
|
||||
'DEEPSEEK_API_KEY': '',
|
||||
'DEEPSEEK_PROXY': '',
|
||||
'DEEPSEEK_MODEL': 'deepseek-chat',
|
||||
'VIRTUAL_APP_ENABLED': False,
|
||||
|
||||
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
|
||||
|
||||
@@ -19,6 +19,7 @@ XPACK_TEMPLATES_DIR = []
|
||||
XPACK_CONTEXT_PROCESSOR = []
|
||||
XPACK_LICENSE_IS_VALID = False
|
||||
XPACK_LICENSE_EDITION = ""
|
||||
XPACK_LICENSE_EDITION_ULTIMATE = False
|
||||
XPACK_LICENSE_INFO = {
|
||||
'corporation': corporation,
|
||||
}
|
||||
|
||||
@@ -222,10 +222,15 @@ ASSET_SIZE = 'small'
|
||||
|
||||
# Chat AI
|
||||
CHAT_AI_ENABLED = CONFIG.CHAT_AI_ENABLED
|
||||
GPT_API_KEY = CONFIG.GPT_API_KEY
|
||||
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
|
||||
|
||||
VIRTUAL_APP_ENABLED = CONFIG.VIRTUAL_APP_ENABLED
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class ResourceDownload(TemplateView):
|
||||
OPENSSH_VERSION=v9.4.0.0
|
||||
TINKER_VERSION=v0.1.6
|
||||
VIDEO_PLAYER_VERSION=0.2.0
|
||||
CLIENT_VERSION=v3.0.0
|
||||
CLIENT_VERSION=v3.0.1
|
||||
"""
|
||||
|
||||
def get_meta_json(self):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Generated by Django 4.1.13 on 2024-10-30 09:38
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -34,7 +35,7 @@ class Migration(migrations.Migration):
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=1024, null=True, verbose_name='Name')),
|
||||
('var_name', models.CharField(help_text="The variable name used in the script has a fixed prefix 'jms_' followed by the input variable name. For example, if the variable name is 'name,' the final generated environment variable will be 'jms_name'.", max_length=1024, null=True, verbose_name='Variable name')),
|
||||
('var_name', models.CharField(help_text="The variable name used in the script will have a fixed prefix jms_ added to the input variable name. For example, if the input variable name is name, the resulting environment variable will be jms_name, and it can be referenced in the script using {{ jms_name }}", max_length=1024, null=True, verbose_name='Variable name')),
|
||||
('default_value', models.CharField(max_length=2048, null=True, verbose_name='Default Value')),
|
||||
('type', models.CharField(default='text', max_length=64, verbose_name='Variable type')),
|
||||
('tips', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Tips')),
|
||||
|
||||
@@ -188,11 +188,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||
|
||||
@property
|
||||
def average_time_cost(self):
|
||||
total_cost = 0
|
||||
finished_count = self.executions.filter(status__in=['success', 'failed']).count()
|
||||
for execution in self.executions.filter(status__in=['success', 'failed']).all():
|
||||
total_cost += execution.time_cost
|
||||
return total_cost / finished_count if finished_count else 0
|
||||
return self.last_execution.time_cost if self.last_execution else 0
|
||||
|
||||
def get_register_task(self):
|
||||
from ..tasks import run_ops_job
|
||||
|
||||
@@ -9,9 +9,9 @@ class Variable(JMSBaseModel):
|
||||
name = models.CharField(max_length=1024, verbose_name=_('Name'), null=True)
|
||||
var_name = models.CharField(
|
||||
max_length=1024, null=True, verbose_name=_('Variable name'),
|
||||
help_text=_("The variable name used in the script has a fixed prefix 'jms_' followed by the input variable "
|
||||
"name. For example, if the variable name is 'name,' the final generated environment variable will "
|
||||
"be 'jms_name'.")
|
||||
help_text=_("The variable name used in the script will have a fixed prefix jms_ added to the input variable "
|
||||
"name. For example, if the input variable name is name, the resulting environment variable will "
|
||||
"be jms_name, and it can be referenced in the script using {{ jms_name }}")
|
||||
)
|
||||
default_value = models.CharField(max_length=2048, verbose_name=_('Default Value'), null=True)
|
||||
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from common.serializers.fields import ReadableHiddenField, LabeledChoiceField, EncryptedField
|
||||
from common.serializers.fields import ReadableHiddenField, LabeledChoiceField
|
||||
from common.serializers.mixin import CommonBulkModelSerializer
|
||||
from ops.const import FieldType
|
||||
from ops.models import Variable, AdHoc, Job, Playbook
|
||||
@@ -20,15 +20,17 @@ class VariableSerializer(CommonBulkModelSerializer):
|
||||
name = serializers.CharField(max_length=1024, label=_('Name'), required=True)
|
||||
var_name = serializers.CharField(
|
||||
max_length=1024, required=True, label=_('Variable name'),
|
||||
help_text=_("The variable name used in the script has a fixed prefix 'jms_' followed by the input variable "
|
||||
"name. For example, if the variable name is 'name,' the final generated environment variable will "
|
||||
"be 'jms_name'.")
|
||||
help_text=_("The variable name used in the script will have a fixed prefix jms_ added to the input variable "
|
||||
"name. For example, if the input variable name is name, the resulting environment variable will "
|
||||
"be jms_name, and it can be referenced in the script using {{ jms_name }}")
|
||||
)
|
||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||
type = LabeledChoiceField(
|
||||
choices=FieldType.choices, default=FieldType.text, label=_("Variable Type")
|
||||
)
|
||||
default_value = serializers.CharField(max_length=2048, label=_('Default Value'), required=False, allow_blank=True)
|
||||
default_value = serializers.CharField(
|
||||
max_length=2048, label=_('Default Value'), required=False, allow_blank=True, allow_null=True
|
||||
)
|
||||
extra_args = serializers.CharField(
|
||||
max_length=1024, label=_("ExtraVars"), required=False, allow_blank=True,
|
||||
help_text=_(
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 ops.serializers.job import JobExecutionSerializer
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from .celery.decorator import (
|
||||
@@ -57,14 +58,13 @@ def _run_ops_job_execution(execution):
|
||||
)
|
||||
)
|
||||
def run_ops_job(job_id):
|
||||
if not settings.SECURITY_COMMAND_EXECUTION:
|
||||
return
|
||||
with tmp_to_root_org():
|
||||
job = get_object_or_none(Job, id=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
|
||||
@@ -92,14 +92,13 @@ def job_execution_task_activity_callback(self, execution_id, *args, **kwargs):
|
||||
)
|
||||
)
|
||||
def run_ops_job_execution(execution_id, **kwargs):
|
||||
if not settings.SECURITY_COMMAND_EXECUTION:
|
||||
return
|
||||
with tmp_to_root_org():
|
||||
execution = get_object_or_none(JobExecution, id=execution_id)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -122,9 +128,10 @@ def tmp_to_builtin_org(system=0, default=0):
|
||||
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:
|
||||
try:
|
||||
set_current_org(org_id)
|
||||
yield
|
||||
finally:
|
||||
set_current_org(ori_org)
|
||||
|
||||
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
#
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.utils.http import is_true
|
||||
from common.utils import lazyproperty, is_uuid
|
||||
from common.exceptions import JMSObjectDoesNotExist
|
||||
from common.utils import 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 +60,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
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ from perms.hands import Node
|
||||
from perms.models import PermNode
|
||||
from perms.utils import UserPermAssetUtil
|
||||
from perms.utils import UserPermNodeUtil
|
||||
from .mixin import RebuildTreeMixin
|
||||
from ..mixin import SelfOrPKUserMixin
|
||||
from ..mixin import SelfOrPKUserMixin, RebuildTreeMixin
|
||||
|
||||
__all__ = [
|
||||
'UserPermedNodesWithAssetsAsTreeApi',
|
||||
|
||||
@@ -23,14 +23,14 @@ class AssetPermissionUtil(object):
|
||||
# group
|
||||
if with_group:
|
||||
groups = user.groups.all()
|
||||
group_perm_ids = self.get_permissions_for_user_groups(groups, flat=True)
|
||||
group_perm_ids = self.get_permissions_for_user_groups(groups, flat=True, with_expired=with_expired)
|
||||
perm_ids.update(group_perm_ids)
|
||||
perms = self.get_permissions(ids=perm_ids, with_expired=with_expired)
|
||||
if flat:
|
||||
return perms.values_list('id', flat=True)
|
||||
return perms
|
||||
|
||||
def get_permissions_for_user_groups(self, user_groups, flat=False):
|
||||
def get_permissions_for_user_groups(self, user_groups, flat=False, with_expired=False):
|
||||
""" 获取用户组的授权规则 """
|
||||
if isinstance(user_groups, list):
|
||||
group_ids = [g.id for g in user_groups]
|
||||
@@ -39,7 +39,7 @@ class AssetPermissionUtil(object):
|
||||
perm_ids = AssetPermission.user_groups.through.objects \
|
||||
.filter(usergroup_id__in=group_ids) \
|
||||
.values_list('assetpermission_id', flat=True).distinct()
|
||||
perms = self.get_permissions(ids=perm_ids)
|
||||
perms = self.get_permissions(ids=perm_ids, with_expired=with_expired)
|
||||
if flat:
|
||||
return perms.values_list('id', flat=True)
|
||||
return perms
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
@@ -40,12 +41,21 @@ class ChatAITestingAPI(GenericAPIView):
|
||||
data={'msg': _('Chat AI is not enabled')}
|
||||
)
|
||||
|
||||
proxy = config['GPT_PROXY']
|
||||
model = config['GPT_MODEL']
|
||||
tp = config['CHAT_AI_TYPE']
|
||||
if tp == ChatAITypeChoices.gpt:
|
||||
url = config['GPT_BASE_URL']
|
||||
api_key = config['GPT_API_KEY']
|
||||
proxy = config['GPT_PROXY']
|
||||
model = config['GPT_MODEL']
|
||||
else:
|
||||
url = config['DEEPSEEK_BASE_URL']
|
||||
api_key = config['DEEPSEEK_API_KEY']
|
||||
proxy = config['DEEPSEEK_PROXY']
|
||||
model = config['DEEPSEEK_MODEL']
|
||||
|
||||
kwargs = {
|
||||
'base_url': config['GPT_BASE_URL'] or None,
|
||||
'api_key': config['GPT_API_KEY'],
|
||||
'base_url': url or None,
|
||||
'api_key': api_key,
|
||||
}
|
||||
try:
|
||||
if proxy:
|
||||
|
||||
@@ -5,3 +5,21 @@ class ImportStatus(TextChoices):
|
||||
ok = 'ok', 'Ok'
|
||||
pending = 'pending', 'Pending'
|
||||
error = 'error', 'Error'
|
||||
|
||||
|
||||
class ChatAITypeChoices(TextChoices):
|
||||
gpt = 'gpt', 'GPT'
|
||||
deep_seek = 'deep-seek', 'DeepSeek'
|
||||
|
||||
|
||||
class GPTModelChoices(TextChoices):
|
||||
gpt_4o_mini = 'gpt-4o-mini', 'gpt-4o-mini'
|
||||
gpt_4o = 'gpt-4o', 'gpt-4o'
|
||||
o3_mini = 'o3-mini', 'o3-mini'
|
||||
o1_mini = 'o1-mini', 'o1-mini'
|
||||
o1 = 'o1', 'o1'
|
||||
|
||||
|
||||
class DeepSeekModelChoices(TextChoices):
|
||||
deepseek_chat = 'deepseek-chat', 'DeepSeek-V3'
|
||||
deepseek_reasoner = 'deepseek-reasoner', 'DeepSeek-R1'
|
||||
|
||||
@@ -10,7 +10,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import signer, get_logger
|
||||
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__)
|
||||
@@ -53,7 +55,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)
|
||||
@@ -66,7 +68,7 @@ class Setting(models.Model):
|
||||
try:
|
||||
v = json.dumps(item, cls=JSONEncoder)
|
||||
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)))
|
||||
@@ -190,3 +192,19 @@ class ChatPrompt(JMSBaseModel):
|
||||
|
||||
def __str__(self):
|
||||
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,
|
||||
}
|
||||
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
|
||||
|
||||
return data
|
||||
|
||||
@@ -3,6 +3,7 @@ from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import EncryptedField
|
||||
from .base import OrgListField
|
||||
from .mixin import LDAPSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer',
|
||||
@@ -36,7 +37,7 @@ class LDAPUserSerializer(serializers.Serializer):
|
||||
status = serializers.JSONField(read_only=True)
|
||||
|
||||
|
||||
class LDAPSettingSerializer(serializers.Serializer):
|
||||
class LDAPSettingSerializer(LDAPSerializerMixin, serializers.Serializer):
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
PREFIX_TITLE = _('LDAP')
|
||||
|
||||
@@ -85,7 +86,7 @@ class LDAPSettingSerializer(serializers.Serializer):
|
||||
)
|
||||
AUTH_LDAP_CACHE_TIMEOUT = serializers.IntegerField(
|
||||
min_value=0, max_value=3600 * 24 * 30 * 12,
|
||||
default=3600 * 24 * 30,
|
||||
default=0,
|
||||
required=False, label=_('User DN cache timeout (s)'),
|
||||
help_text=_(
|
||||
'Caching the User DN obtained during user login authentication can effectively '
|
||||
@@ -103,10 +104,11 @@ class LDAPSettingSerializer(serializers.Serializer):
|
||||
AUTH_LDAP = serializers.BooleanField(required=False, label=_('LDAP'))
|
||||
AUTH_LDAP_SYNC_ORG_IDS = OrgListField()
|
||||
|
||||
def post_save(self):
|
||||
keys = ['AUTH_LDAP_SYNC_IS_PERIODIC', 'AUTH_LDAP_SYNC_INTERVAL', 'AUTH_LDAP_SYNC_CRONTAB']
|
||||
kwargs = {k: self.validated_data[k] for k in keys if k in self.validated_data}
|
||||
if not kwargs:
|
||||
return
|
||||
periodic_key = 'AUTH_LDAP_SYNC_IS_PERIODIC'
|
||||
interval_key = 'AUTH_LDAP_SYNC_INTERVAL'
|
||||
crontab_key = 'AUTH_LDAP_SYNC_CRONTAB'
|
||||
|
||||
@staticmethod
|
||||
def import_task_function(**kwargs):
|
||||
from settings.tasks import import_ldap_user_periodic
|
||||
import_ldap_user_periodic(**kwargs)
|
||||
|
||||
@@ -3,6 +3,8 @@ from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import EncryptedField
|
||||
from .base import OrgListField
|
||||
from .mixin import LDAPSerializerMixin
|
||||
from ops.mixin import PeriodTaskSerializerMixin
|
||||
|
||||
__all__ = ['LDAPHATestConfigSerializer', 'LDAPHASettingSerializer']
|
||||
|
||||
@@ -18,7 +20,7 @@ class LDAPHATestConfigSerializer(serializers.Serializer):
|
||||
AUTH_LDAP_HA = serializers.BooleanField(required=False)
|
||||
|
||||
|
||||
class LDAPHASettingSerializer(serializers.Serializer):
|
||||
class LDAPHASettingSerializer(LDAPSerializerMixin, serializers.Serializer):
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
PREFIX_TITLE = _('LDAP HA')
|
||||
|
||||
@@ -67,7 +69,7 @@ class LDAPHASettingSerializer(serializers.Serializer):
|
||||
)
|
||||
AUTH_LDAP_HA_CACHE_TIMEOUT = serializers.IntegerField(
|
||||
min_value=0, max_value=3600 * 24 * 30 * 12,
|
||||
default=3600 * 24 * 30,
|
||||
default=0,
|
||||
required=False, label=_('User DN cache timeout (s)'),
|
||||
help_text=_(
|
||||
'Caching the User DN obtained during user login authentication can effectively'
|
||||
@@ -85,10 +87,11 @@ class LDAPHASettingSerializer(serializers.Serializer):
|
||||
AUTH_LDAP_HA = serializers.BooleanField(required=False, label=_('LDAP HA'))
|
||||
AUTH_LDAP_HA_SYNC_ORG_IDS = OrgListField()
|
||||
|
||||
def post_save(self):
|
||||
keys = ['AUTH_LDAP_HA_SYNC_IS_PERIODIC', 'AUTH_LDAP_HA_SYNC_INTERVAL', 'AUTH_LDAP_HA_SYNC_CRONTAB']
|
||||
kwargs = {k: self.validated_data[k] for k in keys if k in self.validated_data}
|
||||
if not kwargs:
|
||||
return
|
||||
periodic_key = 'AUTH_LDAP_HA_SYNC_IS_PERIODIC'
|
||||
interval_key = 'AUTH_LDAP_HA_SYNC_INTERVAL'
|
||||
crontab_key = 'AUTH_LDAP_HA_SYNC_CRONTAB'
|
||||
|
||||
@staticmethod
|
||||
def import_task_function(**kwargs):
|
||||
from settings.tasks import import_ldap_ha_user_periodic
|
||||
import_ldap_ha_user_periodic(**kwargs)
|
||||
|
||||
21
apps/settings/serializers/auth/mixin.py
Normal file
21
apps/settings/serializers/auth/mixin.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from ops.mixin import PeriodTaskSerializerMixin
|
||||
|
||||
|
||||
class LDAPSerializerMixin:
|
||||
def validate(self, attrs):
|
||||
is_periodic = attrs.get(self.periodic_key)
|
||||
crontab = attrs.get(self.crontab_key)
|
||||
interval = attrs.get(self.interval_key)
|
||||
if is_periodic and not any([crontab, interval]):
|
||||
msg = _("Require interval or crontab setting")
|
||||
raise serializers.ValidationError(msg)
|
||||
return super().validate(attrs)
|
||||
|
||||
def post_save(self):
|
||||
keys = [self.periodic_key, self.interval_key, self.crontab_key]
|
||||
kwargs = {k: self.validated_data[k] for k in keys if k in self.validated_data}
|
||||
if not kwargs:
|
||||
return
|
||||
self.import_task_function(**kwargs)
|
||||
@@ -4,7 +4,6 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.const import Protocol
|
||||
from common.serializers.fields import EncryptedField
|
||||
from common.utils import date_expired_default
|
||||
|
||||
@@ -14,6 +13,8 @@ __all__ = [
|
||||
'ChatAISettingSerializer', 'VirtualAppSerializer', 'AmazonSMSerializer',
|
||||
]
|
||||
|
||||
from settings.const import ChatAITypeChoices, GPTModelChoices, DeepSeekModelChoices
|
||||
|
||||
|
||||
class AnnouncementSerializer(serializers.Serializer):
|
||||
ID = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
@@ -119,16 +120,17 @@ class AmazonSMSerializer(serializers.Serializer):
|
||||
|
||||
class ChatAISettingSerializer(serializers.Serializer):
|
||||
PREFIX_TITLE = _('Chat AI')
|
||||
API_MODEL = Protocol.gpt_protocols()[Protocol.chatgpt]['setting']['api_mode']
|
||||
GPT_MODEL_CHOICES = API_MODEL['choices']
|
||||
GPT_MODEL_DEFAULT = API_MODEL['default']
|
||||
|
||||
CHAT_AI_ENABLED = serializers.BooleanField(
|
||||
required=False, label=_('Chat AI')
|
||||
)
|
||||
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=_('GPT Base URL'),
|
||||
help_text=_('The base URL of the GPT service. For example: https://api.openai.com/v1')
|
||||
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'),
|
||||
@@ -138,7 +140,23 @@ class ChatAISettingSerializer(serializers.Serializer):
|
||||
help_text=_('The proxy server address of the GPT service. For example: http://ip:port')
|
||||
)
|
||||
GPT_MODEL = serializers.ChoiceField(
|
||||
default=GPT_MODEL_DEFAULT, choices=GPT_MODEL_CHOICES, label=_("GPT Model"), required=False,
|
||||
default=GPTModelChoices.gpt_4o_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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
PASSWORD_RULE = serializers.DictField()
|
||||
SECURITY_SESSION_SHARE = serializers.BooleanField()
|
||||
XPACK_LICENSE_IS_VALID = serializers.BooleanField()
|
||||
XPACK_LICENSE_EDITION_ULTIMATE = serializers.BooleanField()
|
||||
FACE_RECOGNITION_ENABLED = serializers.BooleanField()
|
||||
XPACK_LICENSE_INFO = serializers.DictField()
|
||||
HELP_DOCUMENT_URL = serializers.CharField()
|
||||
HELP_SUPPORT_URL = serializers.CharField()
|
||||
@@ -61,6 +63,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
VAULT_ENABLED = serializers.BooleanField()
|
||||
VIRTUAL_APP_ENABLED = serializers.BooleanField()
|
||||
CHAT_AI_ENABLED = serializers.BooleanField()
|
||||
CHAT_AI_TYPE = serializers.CharField()
|
||||
GPT_MODEL = serializers.CharField()
|
||||
FILE_UPLOAD_SIZE_LIMIT_MB = serializers.IntegerField()
|
||||
FTP_FILE_MAX_STORE = serializers.IntegerField()
|
||||
|
||||
@@ -9,7 +9,7 @@ from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
from ops.celery.decorator import after_app_ready_start
|
||||
from ops.celery.utils import (
|
||||
create_or_update_celery_periodic_tasks
|
||||
create_or_update_celery_periodic_tasks, disable_celery_periodic_task
|
||||
)
|
||||
from orgs.models import Organization
|
||||
from settings.notifications import LDAPImportMessage
|
||||
@@ -90,9 +90,12 @@ def import_ldap_ha_user():
|
||||
|
||||
|
||||
def register_periodic_task(task_name, task_func, interval_key, enabled_key, crontab_key, **kwargs):
|
||||
interval = kwargs.get(interval_key, settings.AUTH_LDAP_SYNC_INTERVAL)
|
||||
enabled = kwargs.get(enabled_key, settings.AUTH_LDAP_SYNC_IS_PERIODIC)
|
||||
crontab = kwargs.get(crontab_key, settings.AUTH_LDAP_SYNC_CRONTAB)
|
||||
interval = kwargs.get(interval_key, getattr(settings, interval_key))
|
||||
enabled = kwargs.get(enabled_key, getattr(settings, enabled_key))
|
||||
crontab = kwargs.get(crontab_key, getattr(settings, crontab_key))
|
||||
|
||||
if not enabled:
|
||||
disable_celery_periodic_task(task_name)
|
||||
|
||||
if isinstance(interval, int):
|
||||
interval = interval * 3600
|
||||
|
||||
@@ -147,6 +147,9 @@ function activeNav(prefix) {
|
||||
}
|
||||
var path = document.location.pathname;
|
||||
path = path.replace(prefix, '');
|
||||
if (path === '/core/download/') {
|
||||
return
|
||||
}
|
||||
var urlArray = path.split("/");
|
||||
var app = urlArray[1];
|
||||
var resource = urlArray[2];
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- Custom and plugin javascript -->
|
||||
<script src="{% static "js/plugins/toastr/toastr.min.js" %}"></script>
|
||||
<script src="{% static "js/inspinia.js" %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}?v=9"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}?v=10"></script>
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/select2/i18n/zh-CN.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/markdown-it.min.js' %}"></script>
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if INTERFACE.footer_content %}
|
||||
<style>
|
||||
.markdown-footer a {
|
||||
color: inherit;
|
||||
color: #428bca;
|
||||
}
|
||||
|
||||
.markdown-footer {
|
||||
@@ -46,12 +46,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>
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
from settings.models import get_chatai_data
|
||||
from terminal.const import TerminalType as TypeChoices
|
||||
from users.models import User
|
||||
from .status import Status
|
||||
@@ -120,11 +121,13 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
|
||||
|
||||
@staticmethod
|
||||
def get_chat_ai_setting():
|
||||
data = get_chatai_data()
|
||||
return {
|
||||
'GPT_BASE_URL': settings.GPT_BASE_URL,
|
||||
'GPT_API_KEY': settings.GPT_API_KEY,
|
||||
'GPT_PROXY': settings.GPT_PROXY,
|
||||
'GPT_MODEL': settings.GPT_MODEL,
|
||||
'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,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ def create_ticket_flow_and_approval_rule(apps, schema_editor):
|
||||
ApprovalRule = apps.get_model("tickets", "ApprovalRule")
|
||||
|
||||
super_user = User.objects.get(username='admin')
|
||||
flow = TicketFlow.objects.create(created_by='System', type='apply_asset', org_id=org_id)
|
||||
flow, created = TicketFlow.objects.get_or_create(created_by='System', type='apply_asset', org_id=org_id)
|
||||
rule_instance = ApprovalRule.objects.create(strategy='super_admin')
|
||||
rule_instance.assignees.set([super_user])
|
||||
flow.rules.set([rule_instance, ])
|
||||
|
||||
@@ -424,7 +424,8 @@ class Ticket(StatusMixin, JMSBaseModel):
|
||||
new_values.append(str(new_value))
|
||||
value = ', '.join(new_values)
|
||||
elif name == 'org_id':
|
||||
value = Organization.get_instance(value).name
|
||||
org = Organization.get_instance(value)
|
||||
value = org.name if org else ''
|
||||
elif isinstance(value, list):
|
||||
value = ', '.join(value)
|
||||
return value
|
||||
|
||||
@@ -10,7 +10,6 @@ from common.utils import get_object_or_none
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from users.notifications import (
|
||||
ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg,
|
||||
ResetPublicKeySuccessMsg,
|
||||
)
|
||||
from .mixins import UserQuerysetMixin
|
||||
from .. import serializers
|
||||
|
||||
Reference in New Issue
Block a user