diff --git a/apps/authentication/templates/authentication/agreement.html b/apps/authentication/templates/authentication/agreement.html
new file mode 100644
index 000000000..74b4d937b
--- /dev/null
+++ b/apps/authentication/templates/authentication/agreement.html
@@ -0,0 +1,129 @@
+
+
+
+
+ User Agreement & Privacy Policy
+
+
+
+
+
+
+
User Agreement & Privacy Policy
+
+
+ Version: 2026-01
+ Effective Date: Feb 1, 2026
+
+
+
1. User Agreement
+
+
1.1 Product Nature
+
+ This system is an enterprise-grade system deployed in a private environment. All
+ deployment environments and data are fully controlled by the customer.
+
+
+
1.2 User Obligations
+
+ - Use the system only within authorized scope;
+ - Do not perform illegal or unauthorized activities;
+ - Do not bypass or interfere with security controls and audit mechanisms.
+
+
+
1.3 Account Security
+
+ Users are responsible for safeguarding their credentials. Risks caused by improper
+ credential management shall be borne by the user or customer.
+
+
+
2. Privacy Policy
+
+
2.1 Scope
+
+ This policy applies to the privately deployed version of this system. All personal
+ data is stored within the customer's own environment. The software provider does not
+ access, store, or transmit such data.
+
+
+
2.2 Personal Information We Process
+
+ - Username
+ - Phone number
+ - Email address
+
+
+
2.3 Authentication Information
+
+ Passwords are never stored in plaintext. Only irreversible encrypted password hashes
+ are stored for authentication.
+
+
+
2.4 Facial Recognition Data
+
+ Facial recognition is an optional premium feature and is disabled by default. If
+ enabled, facial image or feature data will be processed solely for identity
+ verification.
+
+
+ Such data is considered sensitive personal information and is stored only within the
+ customer's local environment.
+
+
+
2.5 Cookies and Similar Technologies
+
+ Necessary cookies and session identifiers are used to maintain login sessions and
+ system security. These technologies are not used for advertising or cross-site
+ tracking.
+
+
+
2.6 Policy Updates
+
Material changes to this policy will require renewed user consent.
+
+
+
diff --git a/apps/authentication/templates/authentication/agreement_zh.html b/apps/authentication/templates/authentication/agreement_zh.html
new file mode 100644
index 000000000..bc96d00e5
--- /dev/null
+++ b/apps/authentication/templates/authentication/agreement_zh.html
@@ -0,0 +1,165 @@
+
+
+
+
+ 用户协议与隐私政策
+
+
+
+
+
+
用户协议与隐私政策
+
+
+ 版本号:2026-01
+ 生效日期:2026-02-01
+
+
+
本软件为企业级私有化部署系统,仅供客户内部授权人员使用。
+
+
一、用户协议
+
+
1. 产品性质
+
+ 本系统为企业级运维与安全管理系统,
+ 采用私有化部署方式运行,系统的部署环境、运行环境及数据均由客户自行控制。
+
+
+
2. 用户行为规范
+
+ - 用户应在客户授权范围内使用本系统;
+ - 不得利用本系统从事违法、违规或未授权行为;
+ - 不得破坏、规避或干扰系统的安全审计与控制机制。
+
+
+
3. 账号与凭证安全
+
+ 用户应妥善保管自身账号及认证凭证。
+ 因用户个人原因导致的账号安全风险,由用户或客户自行承担。
+
+
+
4. 协议变更
+
如本协议内容发生实质性变更,系统将提示用户重新确认。
+
+
+
+
二、隐私政策
+
+
1. 适用范围
+
+ 本隐私政策适用于本系统私有化部署版本。 系统所产生的数据均存储于客户自有环境中,
+ 软件提供方不接触、不存储、不回传客户业务数据或个人信息。
+
+
+
2. 我们收集和使用的个人信息
+
+
2.1 基本账号信息
+
+
用于账号管理、身份识别与安全通知。
+
+
2.2 身份认证信息
+
系统不会以明文形式存储用户密码, 仅以不可逆加密方式保存密码摘要,用于身份校验。
+
+
2.3 日志与审计信息
+
+ - 登录时间、登录 IP
+ - 用户操作审计日志
+
+
用于安全审计、合规留痕与问题追溯。
+
+
3. 敏感个人信息的处理(人脸识别)
+
人脸识别功能为可选增值服务,默认不开启, 仅在客户或用户主动启用后生效。
+
+ 如启用该功能,系统将采集并处理用户的人脸图像或特征信息,
+ 用于身份认证与防止账号冒用。
+
+
+ 上述信息属于敏感个人信息, 仅存储和处理于客户自有部署环境中,
+ 不会传输至软件提供方服务器。
+
+
用户或客户管理员可随时关闭人脸识别功能, 并删除已采集的人脸信息。
+
+
4. Cookie 与同类技术
+
+ 为保障系统正常运行、登录状态维持及安全防护, 系统会在用户设备中存储必要的 Cookie
+ 或会话标识 (如 Session、CSRF Token)。
+
+
上述技术仅用于系统功能实现, 不用于广告投放或跨站追踪。
+
+
5. 数据存储位置与期限
+
系统中的个人信息均存储于客户自有环境中, 具体保存期限由客户根据自身管理要求决定。
+
+
6. 信息共享、转让与披露
+
+ 软件提供方不会共享、转让或披露系统中的个人信息,
+ 但法律法规或司法机关另有要求的除外。
+
+
+
7. 用户权利
+
+ - 查询、更正个人信息
+ - 删除个人信息(在权限允许范围内)
+ - 关闭或删除人脸识别信息
+
+
+
8. 安全措施
+
+ - 加密存储与传输
+ - 访问控制与权限管理
+ - 操作审计与日志留存
+
+
+
9. 隐私政策更新
+
如本隐私政策发生实质性变更, 系统将要求用户重新确认后方可继续使用。
+
+
+
diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py
index 7c9e1a55b..aca5e60c5 100644
--- a/apps/authentication/urls/view_urls.py
+++ b/apps/authentication/urls/view_urls.py
@@ -84,5 +84,6 @@ urlpatterns = [
path('captcha/', include('captcha.urls')),
- path('oauth2-provider/', include(('authentication.backends.oauth2_provider.urls', 'authentication'), namespace='oauth2-provider'))
+ path('oauth2-provider/', include(('authentication.backends.oauth2_provider.urls', 'authentication'), namespace='oauth2-provider')),
+ path('agreement/', views.AgreementView.as_view(), name='agreement'),
]
diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py
index 3c51a1c85..557ad14b1 100644
--- a/apps/authentication/views/__init__.py
+++ b/apps/authentication/views/__init__.py
@@ -7,3 +7,4 @@ from .login import *
from .mfa import *
from .slack import *
from .wecom import *
+from .agreement import *
\ No newline at end of file
diff --git a/apps/authentication/views/agreement.py b/apps/authentication/views/agreement.py
new file mode 100644
index 000000000..ddc6086b0
--- /dev/null
+++ b/apps/authentication/views/agreement.py
@@ -0,0 +1,13 @@
+from django.views.generic import TemplateView
+from django.utils.translation import get_language
+
+__all__ = ['AgreementView']
+
+class AgreementView(TemplateView):
+
+ def get_template_names(self):
+ current_lang = get_language() or 'zh-cn'
+ if current_lang.startswith('zh'):
+ return 'authentication/agreement_zh.html'
+ else:
+ return 'authentication/agreement.html'
diff --git a/apps/common/utils/zip.py b/apps/common/utils/zip.py
index b11443d3d..4cac77c96 100644
--- a/apps/common/utils/zip.py
+++ b/apps/common/utils/zip.py
@@ -2,6 +2,7 @@ import os
import stat
import hashlib
from pathlib import Path
+from rest_framework.exceptions import ValidationError
from zipfile import ZipFile, BadZipFile
@@ -14,7 +15,7 @@ MAX_TOTAL_SIZE = 1 * 1024 * 1024 * 1024 # 1GB
MAX_COMPRESSION_RATIO = 100 # 解压 / 压缩
-class ZipSecurityError(Exception):
+class ZipSecurityError(ValidationError):
pass
diff --git a/apps/i18n/lina/en.json b/apps/i18n/lina/en.json
index 5decec9a1..158b7c824 100644
--- a/apps/i18n/lina/en.json
+++ b/apps/i18n/lina/en.json
@@ -1639,5 +1639,9 @@
"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"
-}
\ No newline at end of file
+ "Revoke": "Revoke",
+ "ReadAgreeTo": "I have read and agree to the",
+ "TermsOfService": "Terms of Service",
+ "PrivacyPolicy": "Privacy Policy",
+ "and": "and"
+}
diff --git a/apps/i18n/lina/zh.json b/apps/i18n/lina/zh.json
index f2f4b107d..fd786f577 100644
--- a/apps/i18n/lina/zh.json
+++ b/apps/i18n/lina/zh.json
@@ -1650,5 +1650,9 @@
"AccessTokenTip": "访问令牌是通过 JumpServer 客户端使用 OAuth2(授权码授权)流程生成的临时凭证,用于访问受保护的资源。",
"Revoke": "撤销",
"AccountSecretReadDisabled": "账号密码读取功能已被管理员禁用",
- "Automations": "自动化"
-}
\ No newline at end of file
+ "Automations": "自动化",
+ "ReadAgreeTo": "我已阅读并同意",
+ "TermsOfService": "服务条款",
+ "PrivacyPolicy": "隐私政策",
+ "and": "和"
+}
diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py
index 073d1161d..25f3e6484 100644
--- a/apps/ops/api/playbook.py
+++ b/apps/ops/api/playbook.py
@@ -1,6 +1,5 @@
import os
import shutil
-import zipfile
from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
@@ -13,6 +12,7 @@ from common.api.generic import JMSBulkModelViewSet
from common.exceptions import JMSException
from common.permissions import IsOwnerOrAdminWritable
from common.utils.http import is_true
+from common.utils.zip import safe_extract_zip
from rbac.permissions import RBACPermission
from ..const import Scope
from ..exception import PlaybookNoValidEntry
@@ -27,9 +27,7 @@ from django.utils._os import safe_join
def unzip_playbook(src, dest):
- fz = zipfile.ZipFile(src, 'r')
- for file in fz.namelist():
- fz.extract(file, dest)
+ safe_extract_zip(src, dest)
class PlaybookViewSet(JMSBulkModelViewSet):
diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py
index 0bbdc8fa3..b86e119da 100644
--- a/apps/terminal/api/applet/applet.py
+++ b/apps/terminal/api/applet/applet.py
@@ -2,7 +2,6 @@ import os
import os.path
import re
import shutil
-import zipfile
from typing import Callable
from django.conf import settings
@@ -22,6 +21,7 @@ from common.utils import is_uuid
from common.utils.http import is_true
from terminal import serializers
from terminal.models import AppletPublication, Applet
+from common.utils.zip import safe_extract_zip
__all__ = ['AppletViewSet', 'AppletPublicationViewSet']
@@ -47,10 +47,7 @@ class DownloadUploadMixin:
shutil.rmtree(extract_to)
try:
- with zipfile.ZipFile(path) as zp:
- if zp.testzip() is not None:
- raise ValidationError({'error': _('Invalid zip file')})
- zp.extractall(extract_to)
+ safe_extract_zip(path, extract_to)
except RuntimeError as e:
raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)})
diff --git a/apps/terminal/api/virtualapp/virtualapp.py b/apps/terminal/api/virtualapp/virtualapp.py
index 7b4c5b9b2..63e7476d3 100644
--- a/apps/terminal/api/virtualapp/virtualapp.py
+++ b/apps/terminal/api/virtualapp/virtualapp.py
@@ -1,6 +1,5 @@
import os.path
import shutil
-import zipfile
from typing import Callable
from django.core.files.storage import default_storage
@@ -16,6 +15,7 @@ from common.api import JMSBulkModelViewSet
from common.serializers import FileSerializer
from terminal import serializers
from terminal.models import VirtualAppPublication, VirtualApp
+from common.utils.zip import safe_extract_zip
__all__ = ['VirtualAppViewSet', 'VirtualAppPublicationViewSet']
@@ -38,10 +38,7 @@ class UploadMixin:
if os.path.exists(extract_to):
shutil.rmtree(extract_to)
try:
- with zipfile.ZipFile(path) as zp:
- if zp.testzip() is not None:
- raise ValidationError({'error': _('Invalid zip file')})
- zp.extractall(extract_to)
+ safe_extract_zip(path, extract_to)
except RuntimeError as e:
raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)})
tmp_dir = safe_join(extract_to, file.name.replace('.zip', ''))