From 8eff94a4f5c8d8c61351a3580fef57d50e3d94c5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 15 Jun 2026 03:19:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(awooop):=20=E7=A7=BB=E9=99=A4=20tenants=20?= =?UTF-8?q?=E5=85=AC=E9=96=8B=E5=85=A7=E9=83=A8=E7=8B=80=E6=85=8B=E7=A2=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/platform_operator_service.py | 24 +++++- .../test_awooop_tenant_asset_inventory.py | 14 ++++ apps/web/messages/en.json | 13 ++- apps/web/messages/zh-TW.json | 13 ++- .../src/app/[locale]/awooop/tenants/page.tsx | 80 ++++++++++++------- .../security-mirror-progress-guard.py | 6 ++ 6 files changed, 115 insertions(+), 35 deletions(-) diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index f15ba5c0..0df65eb6 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -726,6 +726,26 @@ def _source_scope_id(index: int) -> str: return f"SRC-{index:03d}" +def _public_source_risk(value: Any) -> str: + risk = str(value or "").strip().lower() + if risk in {"high", "medium", "low"}: + return risk + return "unknown" + + +def _public_source_readiness(value: Any) -> str: + readiness = str(value or "").strip().lower() + if "refs" in readiness and "parity" in readiness: + return "need_refs_evidence" + if "target" in readiness and "decision" in readiness: + return "need_target_decision" + if "internal_remote" in readiness or ("remote" in readiness and "decision" in readiness): + return "need_internal_remote_decision" + if "scope" in readiness and "review" in readiness: + return "need_scope_review" + return "need_owner_evidence" + + def _public_surface_source_refs(surface: Mapping[str, Any]) -> list[str]: return [f"SRCREF-{index:03d}" for index, _ in enumerate(surface.get("source_keys") or [], start=1)] @@ -778,8 +798,8 @@ def _build_source_repo_assets( "product_name": product["product_name"], "category": product["category"], "scope_status": row.get("scope_status") or "unknown", - "readiness_state": row.get("readiness_state") or "unknown", - "risk": row.get("risk") or "UNKNOWN", + "readiness_state": _public_source_readiness(row.get("readiness_state")), + "risk": _public_source_risk(row.get("risk")), "primary_ready": bool(row.get("primary_ready")), "blocker_count": len(row.get("blockers") or []), "runtime_gate_count": 0, diff --git a/apps/api/tests/test_awooop_tenant_asset_inventory.py b/apps/api/tests/test_awooop_tenant_asset_inventory.py index 9947d795..43a04ffa 100644 --- a/apps/api/tests/test_awooop_tenant_asset_inventory.py +++ b/apps/api/tests/test_awooop_tenant_asset_inventory.py @@ -90,7 +90,21 @@ def test_tenant_asset_inventory_merges_products_routes_and_repos() -> None: inventory_payload = json.dumps(inventory, ensure_ascii=False) assert "owenhytsai" not in inventory_payload assert "nexu-io" not in inventory_payload + assert "blocked_waiting_" not in inventory_payload + assert "observe_scope_review" not in inventory_payload assert all(marker not in inventory_payload for marker in FORBIDDEN_PUBLIC_MARKERS) + assert {item["risk"] for item in inventory["source_repos"]}.issubset( + {"high", "medium", "low", "unknown"} + ) + assert {item["readiness_state"] for item in inventory["source_repos"]}.issubset( + { + "need_refs_evidence", + "need_target_decision", + "need_internal_remote_decision", + "need_scope_review", + "need_owner_evidence", + } + ) assert all(item["runtime_gate_count"] == 0 for item in inventory["source_repos"]) assert all(item["action_button_count"] == 0 for item in inventory["public_routes"]) assert all(item["runtime_gate_count"] == 0 for item in inventory["products"]) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 33c12e4c..1929642a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -8595,7 +8595,7 @@ "globalAssets": { "eyebrow": "全域納管", "title": "全域產品資產台帳", - "subtitle": "把租戶資料表、正式網站入口、前後台產品、平台工具、設計系統、AI 工具與原始碼管控候選合併到同一個只讀視圖;前台只顯示脫敏範圍代號,不揭露原始 owner / namespace,且這不是建立專案庫、改路由、部署、掃描或執行期授權。", + "subtitle": "把租戶資料表、正式網站入口、前後台產品、平台工具、設計系統、AI 工具與原始碼管控候選合併到同一個只讀視圖;前台只顯示脫敏範圍代號,不揭露原始負責人或命名空間,且這不是建立專案庫、改路由、部署、掃描或執行期授權。", "loading": "讀取資產盤", "itemsUnit": "項", "productsTitle": "產品 / 專案納管", @@ -8663,6 +8663,17 @@ "ownerResponseRequired": "待負責人回覆", "unknown": "待分類" }, + "boundaryItems": { + "readOnly": "目前只建立只讀資產索引,不修改租戶、路由、主機或專案庫。", + "redactedIdentity": "前台只顯示範圍代號,不揭露原始負責人、命名空間或完整專案庫名稱。", + "noRawRepository": "原始碼來源只保留脫敏對照與證據參照,正式頁不顯示完整來源字串。", + "ownerResponseZero": "負責人回覆仍未接受,不能把候選範圍視為已核准。", + "runtimeGateZero": "執行期閘門仍為 0,不啟動掃描、修復、部署或主機操作。", + "noActionButtons": "此頁不提供任何可執行按鈕。", + "noRepoCreation": "未取得正式決策前,不建立專案庫、不改可見性、不同步分支或標籤。", + "noRouteChange": "未取得路由負責人證據前,不改 Nginx、公開入口或上游設定。", + "noDeployment": "此頁只顯示治理狀態,不代表部署、主要來源切換或正式資安接受。" + }, "sourceRisk": { "high": "高風險", "medium": "中風險", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 33c12e4c..1929642a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -8595,7 +8595,7 @@ "globalAssets": { "eyebrow": "全域納管", "title": "全域產品資產台帳", - "subtitle": "把租戶資料表、正式網站入口、前後台產品、平台工具、設計系統、AI 工具與原始碼管控候選合併到同一個只讀視圖;前台只顯示脫敏範圍代號,不揭露原始 owner / namespace,且這不是建立專案庫、改路由、部署、掃描或執行期授權。", + "subtitle": "把租戶資料表、正式網站入口、前後台產品、平台工具、設計系統、AI 工具與原始碼管控候選合併到同一個只讀視圖;前台只顯示脫敏範圍代號,不揭露原始負責人或命名空間,且這不是建立專案庫、改路由、部署、掃描或執行期授權。", "loading": "讀取資產盤", "itemsUnit": "項", "productsTitle": "產品 / 專案納管", @@ -8663,6 +8663,17 @@ "ownerResponseRequired": "待負責人回覆", "unknown": "待分類" }, + "boundaryItems": { + "readOnly": "目前只建立只讀資產索引,不修改租戶、路由、主機或專案庫。", + "redactedIdentity": "前台只顯示範圍代號,不揭露原始負責人、命名空間或完整專案庫名稱。", + "noRawRepository": "原始碼來源只保留脫敏對照與證據參照,正式頁不顯示完整來源字串。", + "ownerResponseZero": "負責人回覆仍未接受,不能把候選範圍視為已核准。", + "runtimeGateZero": "執行期閘門仍為 0,不啟動掃描、修復、部署或主機操作。", + "noActionButtons": "此頁不提供任何可執行按鈕。", + "noRepoCreation": "未取得正式決策前,不建立專案庫、不改可見性、不同步分支或標籤。", + "noRouteChange": "未取得路由負責人證據前,不改 Nginx、公開入口或上游設定。", + "noDeployment": "此頁只顯示治理狀態,不代表部署、主要來源切換或正式資安接受。" + }, "sourceRisk": { "high": "高風險", "medium": "中風險", diff --git a/apps/web/src/app/[locale]/awooop/tenants/page.tsx b/apps/web/src/app/[locale]/awooop/tenants/page.tsx index 63feb256..96ed4399 100644 --- a/apps/web/src/app/[locale]/awooop/tenants/page.tsx +++ b/apps/web/src/app/[locale]/awooop/tenants/page.tsx @@ -243,6 +243,18 @@ const ownerResponseValidationTenantBoundaries = [ "action_buttons_allowed=false", ]; +const globalAssetBoundaryItems = [ + "readOnly", + "redactedIdentity", + "noRawRepository", + "ownerResponseZero", + "runtimeGateZero", + "noActionButtons", + "noRepoCreation", + "noRouteChange", + "noDeployment", +]; + const ASSET_STATUS_TONES: Record = { verified: "steady", read_only_visible: "steady", @@ -268,26 +280,6 @@ const ASSET_STATUS_KEYS: Record = { owner_response_required: "ownerResponseRequired", }; -const SOURCE_RISK_KEYS: Record = { - HIGH: "high", - MEDIUM: "medium", - LOW: "low", -}; - -const SOURCE_READINESS_KEYS: Record = { - blocked_waiting_refs_parity: "refsParity", - blocked_waiting_target_decision: "targetDecision", - blocked_waiting_internal_remote_decision: "internalRemoteDecision", - observe_scope_review: "scopeReview", -}; - -const SOURCE_ACTION_KEYS: Record = { - blocked_waiting_refs_parity: "refsParity", - blocked_waiting_target_decision: "targetDecision", - blocked_waiting_internal_remote_decision: "internalRemoteDecision", - observe_scope_review: "scopeReview", -}; - function assetToneClass(tone: AssetTone) { if (tone === "steady") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; if (tone === "locked") return "border-[#b7add8] bg-[#f8f6fc] text-[#5b4b91]"; @@ -315,25 +307,52 @@ function assetCategoryLabel(category: string, t: ReturnType) { - const key = SOURCE_RISK_KEYS[risk] ?? "unknown"; - return t(`sourceRisk.${key}` as never); + return t(`sourceRisk.${sourceRiskKey(risk)}` as never); } function sourceReadinessLabel(readiness: string, t: ReturnType) { - const key = SOURCE_READINESS_KEYS[readiness] ?? "unknown"; + const key = sourceReadinessKey(readiness); return t(`sourceReadiness.${key}` as never); } function sourceActionLabel(readiness: string, t: ReturnType) { - const key = SOURCE_ACTION_KEYS[readiness] ?? "unknown"; + const key = sourceReadinessKey(readiness); return t(`sourceActions.${key}` as never); } function sourceRiskClass(risk: string) { - if (risk === "HIGH") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"; - if (risk === "MEDIUM") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; - if (risk === "LOW") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; + const key = sourceRiskKey(risk); + if (key === "high") return "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"; + if (key === "medium") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"; + if (key === "low") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"; } @@ -812,7 +831,6 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento const products = inventory?.products ?? []; const publicRoutes = inventory?.public_routes ?? []; const sourceRepos = inventory?.source_repos ?? []; - const boundaries = inventory?.boundaries ?? []; const metrics = [ { key: "products", @@ -943,9 +961,9 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento

{t("boundaryTitle")}

{t("boundaryLead")}

-
- {boundaries.slice(0, 9).map((boundary) => ( - {boundary} +
+ {globalAssetBoundaryItems.map((item) => ( + {t(`boundaryItems.${item}` as never)} ))}
diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 7404c68a..76ad9154 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -11683,6 +11683,9 @@ def validate(root: Path) -> None: "{repo.risk}", "{repo.readiness_state}", "namespace_redacted={String(repo.source_namespace_redacted)}", + "repo_owner_namespace_redacted=true", + "raw_repository_namespace_visible=false", + "public_api_raw_repo_namespace_allowed=false", ]: assert_text_not_contains("awooop_tenants_page.raw_source_control_status_render", awooop_tenants_page, text) assert_text_not_contains( @@ -11722,6 +11725,9 @@ def validate(root: Path) -> None: "待參照一致性證據", "待目標與可見性決策", "未接受前不得建立專案庫或改可見性", + "不揭露原始負責人或命名空間", + "不修改租戶、路由、主機或專案庫", + "不顯示完整來源字串", ]: assert_text_contains("web_messages.zh-TW.awooop.tenants.global_assets_redaction", tenant_global_assets_messages_zh, text) assert_text_contains("web_messages.en.awooop.tenants.global_assets_redaction", tenant_global_assets_messages_en, text)