fix: security remediation follow-up and OLT button layout improvements
- fix Casdoor JWT verification with correct PythonProject public key - fix iMC TLS cert validation with custom SSL adapter (skip hostname check for non-DNS CN) - add iMC CA cert and Casdoor public key to build context - improve OLT manage page: unify button styles, fix mobile grid spacing, replace el-upload with native input for consistent alignment - swap NTP sync button for duplicate MAC on mobile Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ def verify_casdoor_token(token: str) -> dict:
|
|||||||
header = pyjwt.get_unverified_header(token)
|
header = pyjwt.get_unverified_header(token)
|
||||||
algorithm = header.get("alg")
|
algorithm = header.get("alg")
|
||||||
if algorithm not in {"RS256", "RS384", "RS512"}:
|
if algorithm not in {"RS256", "RS384", "RS512"}:
|
||||||
raise ValueError("Unsupported Casdoor token algorithm")
|
raise ValueError(f"Unsupported Casdoor token algorithm: {algorithm}")
|
||||||
|
|
||||||
return pyjwt.decode(
|
return pyjwt.decode(
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -39,16 +39,52 @@ class IMCService:
|
|||||||
"""iMC REST API 服务封装"""
|
"""iMC REST API 服务封装"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import certifi
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
|
||||||
self.base_url = settings.IMC_API_URL.rstrip('/')
|
self.base_url = settings.IMC_API_URL.rstrip('/')
|
||||||
self.username = settings.IMC_API_USERNAME
|
self.username = settings.IMC_API_USERNAME
|
||||||
self.password = settings.IMC_API_PASSWORD
|
self.password = settings.IMC_API_PASSWORD
|
||||||
self.verify_ssl = settings.IMC_API_VERIFY_SSL
|
|
||||||
self.connect_timeout = settings.IMC_CONNECT_TIMEOUT
|
self.connect_timeout = settings.IMC_CONNECT_TIMEOUT
|
||||||
self.read_timeout = settings.IMC_READ_TIMEOUT
|
self.read_timeout = settings.IMC_READ_TIMEOUT
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.realm = "iMC RESTful Web Services"
|
self.realm = "iMC RESTful Web Services"
|
||||||
self.nonce = None
|
self.nonce = None
|
||||||
self.nc = 1
|
self.nc = 1
|
||||||
|
self._ca_bundle_file = None # temp file for certifi + iMC CA
|
||||||
|
|
||||||
|
if settings.IMC_API_VERIFY_SSL:
|
||||||
|
imc_ca_path = os.path.join(os.path.dirname(__file__), "..", "..", "imc_ca.pem")
|
||||||
|
if os.path.isfile(imc_ca_path):
|
||||||
|
# The iMC self-signed cert uses a non-DNS CN and zero SAN entries,
|
||||||
|
# so hostname matching is impossible. We still enforce full
|
||||||
|
# certificate-chain verification via a combined CA bundle, then
|
||||||
|
# tell urllib3 to skip its own hostname check.
|
||||||
|
with open(imc_ca_path, "rb") as fh:
|
||||||
|
imc_pem = fh.read()
|
||||||
|
self._ca_bundle_file = tempfile.NamedTemporaryFile(suffix=".pem", delete=False)
|
||||||
|
with open(certifi.where(), "rb") as fh:
|
||||||
|
self._ca_bundle_file.write(fh.read())
|
||||||
|
self._ca_bundle_file.write(b"\n")
|
||||||
|
self._ca_bundle_file.write(imc_pem)
|
||||||
|
self._ca_bundle_file.flush()
|
||||||
|
|
||||||
|
_ca_bundle = self._ca_bundle_file.name
|
||||||
|
|
||||||
|
class _IMCAdapter(HTTPAdapter):
|
||||||
|
def cert_verify(self, conn, url, verify, cert):
|
||||||
|
super().cert_verify(conn, url, verify=_ca_bundle, cert=cert)
|
||||||
|
conn.assert_hostname = False
|
||||||
|
|
||||||
|
self.session.mount("https://", _IMCAdapter())
|
||||||
|
self.verify_ssl = True
|
||||||
|
logger.info("iMC TLS verification enabled (hostname check relaxed)")
|
||||||
|
else:
|
||||||
|
self.verify_ssl = True
|
||||||
|
else:
|
||||||
|
self.verify_ssl = False
|
||||||
|
|
||||||
# ── Digest 认证 ──────────────────────────────────────────────────────────
|
# ── Digest 认证 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC+TCCAeGgAwIBAgIEHno+KjANBgkqhkiG9w0BAQsFADAtMQwwCgYDVQQLDANS
|
||||||
|
JkQxHTAbBgNVBAMTFGlNQyBEZXZlbG9wbWVudCBUZWFtMB4XDTE5MDExOTA2Mzkx
|
||||||
|
OVoXDTM5MDExNDA2MzkxOVowLTEMMAoGA1UECwwDUiZEMR0wGwYDVQQDExRpTUMg
|
||||||
|
RGV2ZWxvcG1lbnQgVGVhbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||||
|
AIVGUvmtUD6Gx0AQLi+/voDIuSERGLyTLcQQOBmWVWLfxr0i1PGxuGODylN+30Ud
|
||||||
|
pBY1faRijvUZUWN51dxjXMRLnHUecBuPVyWOv1jPFMd1jcc7KmQ4KY6s43t1cC2R
|
||||||
|
pBcfyBhVmN+tArhcbDJb6MUuN1P5ZGi3E1vor9EY0Gt8TvJ/GgmqgWwlveAC0soH
|
||||||
|
yTTk8UU9m2OikbaZ0dKgQbshna4wrSWw9iyJ5Bao4rZgus3hGZUnznWpPD0/jHC4
|
||||||
|
lI3znCKQPAsnJculZcK/8dm1UYKFx7C3L5z4SnzsLY+rTYXvmGfb2x06wKkkJTIt
|
||||||
|
oepc1U5U/jNlUVbNdA9yhP0CAwEAAaMhMB8wHQYDVR0OBBYEFGErff6AvZVRmgqK
|
||||||
|
DLQxMBGOr0ObMA0GCSqGSIb3DQEBCwUAA4IBAQA9HhuDvrVI4ICyddy5g4DM+bv4
|
||||||
|
NaKoBS0Y5nhANbN/0f0J0Zj32OYtbsJlUZViFQ42LIR7b37x6OgMXRjHJIDg3q+9
|
||||||
|
tkH5S8Q5xmb5Tqq0WcOWQmg0o1OjW5iJNAfKiTffGtc5DjmrOBuCf1P23G3polRB
|
||||||
|
34QxGgnRygEDKy5aYumaXyiL1yMgDLkZ2adg2lWvsBSpvmgzMF5H75Spq4WvK60Z
|
||||||
|
o3EyCpB5ZS4SxwF3JH+LdpsyCc+UsyYW4/v/FVidtCp+nNTgsTA1yI8vcPVyPxPQ
|
||||||
|
SvVFIvuLYpo9cbHLcmQ95LBKjMbhPVHMzsBRVd85xKVjBspRMWeUp7F94W8V
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArvxqOc2fCe611njyX3aG
|
||||||
|
aprZzyU/UhynIHsJhVMq6tDFLPmJCitg5WFHVTOW5KlTzKI894XJNEPSkf4POoai
|
||||||
|
1z4CW/GeRrkN8/XP8/RqyiCQAslei5FH5i1RjqLWijm0qUNsgYk07ZpBw45qT1ti
|
||||||
|
ipJF33L+mr9s+O17ujDDb2EZBzTcuBs1eYhv2fk0bOdYK17Vzzw1StPP0OWmfYev
|
||||||
|
edw6YemqnODk21e6CGcewXhgIX6k8iBaULQLoy0qTR+4nKwJjnCm4DrbSkYvjM7p
|
||||||
|
AW/3bQyJax1VbFw2jRo1CRcoaMRzLcutpe7HaT+wf4TR7cZFcaFZieSsruLySD5X
|
||||||
|
42bVgWDIZtjtXfJnSZc/Fv6IyUi/PzzlWIaxGcqqVHSk3W/w1ubNc+70SQA7fCJP
|
||||||
|
8vh/GcRNbWF3+mH4B1DSMSl7IAxGNyXeQyNq44Gp9T9MeXsvppQxcHd3Tn4krRi5
|
||||||
|
NzWetnRCOH/kQblfK9FK4o1XGkIHVdwCUBoFO4Tlqu3qNCQIkg28Wg6OgdkHrkoc
|
||||||
|
lW98y0y5Wvt0tJtg48cfpgkHZ9SKM0qhedyUBvGV1fd8QAuwL9JbvMwUX7cVOGBJ
|
||||||
|
6rc4Sk11uRGVCnmw5Ed5ORa0w70DrQYe9OoNqYiqmkLh+TvXMhGmYQ+V8aw6ejmA
|
||||||
|
2Efz40ofkK+Z+Pizv9L3wP8CAwEAAQ==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -8,31 +8,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<el-button v-if="!isMobile && can('olt.manage')" type="primary" size="small" @click="openAddDialog">
|
<el-button v-if="!isMobile && can('olt.manage')" type="primary" size="small" @click="openAddDialog">
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="margin-right: 5px">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="btn-icon">
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
添加 OLT
|
添加 OLT
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-upload
|
<el-button v-if="!isMobile && can('olt.manage')" type="success" plain size="small" :loading="importing" @click="triggerImport">
|
||||||
v-if="!isMobile && can('olt.manage')"
|
批量导入
|
||||||
:auto-upload="false"
|
</el-button>
|
||||||
:show-file-list="false"
|
<el-button v-if="!isMobile && can('olt.manage')" plain size="small" @click="downloadTemplate">下载模板</el-button>
|
||||||
:on-change="handleImportFile"
|
<el-button v-if="can('olt.manage')" type="danger" plain size="small" @click="openDuplicateMacs">
|
||||||
accept=".xlsx,.xls"
|
重复 MAC<span v-if="duplicateCount > 0" class="btn-count danger">{{ duplicateCount }}</span>
|
||||||
style="display: inline-block"
|
|
||||||
>
|
|
||||||
<el-button type="success" size="small" :loading="importing">批量导入</el-button>
|
|
||||||
</el-upload>
|
|
||||||
<el-button v-if="!isMobile && can('olt.manage')" size="small" @click="downloadTemplate">下载模板</el-button>
|
|
||||||
<el-button v-if="!isMobile && can('olt.manage')" type="danger" plain size="small" @click="openDuplicateMacs">
|
|
||||||
重复 MAC
|
|
||||||
<el-badge v-if="duplicateCount > 0" :value="duplicateCount" style="margin-left: 4px" />
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="can('olt.manage')" type="warning" plain size="small" @click="openNewDevices">
|
<el-button v-if="can('olt.manage')" type="warning" plain size="small" @click="openNewDevices">
|
||||||
新增设备
|
新增设备<span v-if="newDeviceCount > 0" class="btn-count warning">{{ newDeviceCount }}</span>
|
||||||
<el-badge v-if="newDeviceCount > 0" :value="newDeviceCount" style="margin-left: 4px" />
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="can('olt.manage')" type="info" plain size="small" @click="openNtpSync" :loading="ntpSyncing">
|
<el-button v-if="!isMobile && can('olt.manage')" type="info" plain size="small" @click="openNtpSync" :loading="ntpSyncing">
|
||||||
同步NTP
|
同步NTP
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="can('olt.loopback')" type="danger" plain size="small" @click="runLoopbackDetection" :loading="loopDetecting">
|
<el-button v-if="can('olt.loopback')" type="danger" plain size="small" @click="runLoopbackDetection" :loading="loopDetecting">
|
||||||
@@ -41,6 +32,15 @@
|
|||||||
<el-button v-if="can('olt.discover')" type="primary" plain size="small" @click="runQuickScan" :loading="quickScanning">
|
<el-button v-if="can('olt.discover')" type="primary" plain size="small" @click="runQuickScan" :loading="quickScanning">
|
||||||
快速扫描
|
快速扫描
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<!-- 隐藏的上传组件(批量导入触发) -->
|
||||||
|
<input
|
||||||
|
v-if="!isMobile && can('olt.manage')"
|
||||||
|
ref="importInput"
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
style="display:none"
|
||||||
|
@change="handleImportFile"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -604,6 +604,7 @@ const newDevList = ref([])
|
|||||||
const newDeviceCount = ref(0)
|
const newDeviceCount = ref(0)
|
||||||
const newDevVisible = ref(false)
|
const newDevVisible = ref(false)
|
||||||
const savingDev = ref(null)
|
const savingDev = ref(null)
|
||||||
|
const importInput = ref(null)
|
||||||
const loopDetecting = ref(false)
|
const loopDetecting = ref(false)
|
||||||
const loopVisible = ref(false)
|
const loopVisible = ref(false)
|
||||||
const loopResults = ref([])
|
const loopResults = ref([])
|
||||||
@@ -973,10 +974,16 @@ const showLoopDetail = (oltId) => {
|
|||||||
loopDetailVisible.value = true
|
loopDetailVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImportFile = async (uploadFile) => {
|
const triggerImport = () => {
|
||||||
|
importInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportFile = async (event) => {
|
||||||
|
const file = event.target?.files?.[0] || (event.raw || event)
|
||||||
|
if (!file) return
|
||||||
importing.value = true
|
importing.value = true
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', uploadFile.raw)
|
formData.append('file', file)
|
||||||
try {
|
try {
|
||||||
const { data } = await request.post('/olt/import', formData, {
|
const { data } = await request.post('/olt/import', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
@@ -1077,8 +1084,35 @@ onMounted(() => {
|
|||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin-left: 4px;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-count.danger { background: var(--danger); color: #fff; }
|
||||||
|
.btn-count.warning { background: var(--warning); color: #fff; }
|
||||||
|
|
||||||
|
.header-actions .el-button {
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 数据面板 */
|
/* 数据面板 */
|
||||||
@@ -1526,19 +1560,17 @@ onMounted(() => {
|
|||||||
.header-actions {
|
.header-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions .el-button,
|
|
||||||
.header-actions > * {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端按钮区只显示2列(移动端只剩新增设备、环路检测、快速扫描) */
|
|
||||||
.header-actions {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .el-button {
|
||||||
|
width: 95%;
|
||||||
|
min-height: 44px;
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 端口管理对话框:限制高度支持滚动 */
|
/* 端口管理对话框:限制高度支持滚动 */
|
||||||
|
|||||||
Reference in New Issue
Block a user