From e367ec1fe31d71edd64cd3ad5ed7fbe064f9af1c Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Wed, 16 Sep 2026 14:58:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=97=A8=E7=A6=81=EF=BC=9A?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=9C=9F=E5=AE=9E=E6=8E=A5=E5=8F=A3=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E5=B9=B6=E8=90=BD=E5=9C=B0=E6=9A=82=E8=A1=8C=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CredentialOneTimeDisplaySecurityTest.java | 161 ++++++++++++++++++ .../dms/client/DmsLocationClient.java | 58 ++++++- .../integration/dms/dto/DmsDoorLockDto.java | 20 ++- .../train/integration/dms/dto/DmsRoomDto.java | 10 +- .../AccessAuthorizationOrchestrator.java | 4 +- .../orchestration/ReconciliationResult.java | 15 ++ .../client/DmsContractCompatibilityTest.java | 60 +++++++ .../dms-r3/provisional-business-policy-v0.md | 119 +++++++++++++ .../real-api-probe/RUN-20260916-01/README.md | 32 ++++ .../RUN-20260916-01/adapter-changes.md | 34 ++++ .../RUN-20260916-01/auth-results.md | 36 ++++ .../RUN-20260916-01/contract-matrix.md | 56 ++++++ .../RUN-20260916-01/environment-summary.md | 48 ++++++ .../RUN-20260916-01/mutation-results.md | 43 +++++ .../RUN-20260916-01/next-step.md | 31 ++++ .../RUN-20260916-01/room-lock-results.md | 58 +++++++ .../RUN-20260916-01/security-check.md | 30 ++++ 17 files changed, 806 insertions(+), 9 deletions(-) create mode 100644 RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/CredentialOneTimeDisplaySecurityTest.java create mode 100644 RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/integration/dms/client/DmsContractCompatibilityTest.java create mode 100644 docs/development/dms-r3/provisional-business-policy-v0.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/README.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/adapter-changes.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/auth-results.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/contract-matrix.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/environment-summary.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/mutation-results.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/next-step.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/room-lock-results.md create mode 100644 docs/reports/dms-r3/real-api-probe/RUN-20260916-01/security-check.md diff --git a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/CredentialOneTimeDisplaySecurityTest.java b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/CredentialOneTimeDisplaySecurityTest.java new file mode 100644 index 000000000..c89504294 --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/CredentialOneTimeDisplaySecurityTest.java @@ -0,0 +1,161 @@ +package com.ruoyi.train.integration.dms.security; + +import com.ruoyi.train.domain.TrainAccommodation; +import com.ruoyi.train.domain.TrainDormAuth; +import com.ruoyi.train.domain.TrainEnrollment; +import com.ruoyi.train.domain.TrainExternalOperation; +import com.ruoyi.train.domain.TrainExternalResourceMapping; +import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult; +import com.ruoyi.web.controller.train.TrainDormAuthController; +import org.junit.Test; +import org.springframework.web.bind.annotation.GetMapping; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * 验证 R.3 Provisional Business Policy V0 中的凭证一次性安全展示策略 (ADMIN_ONE_TIME_DISPLAY) + * 验证指标: + * 1. DB_PLAINTEXT_COUNT = 0 + * 2. LOG_PLAINTEXT_EXPOSURE = 0 + * 3. HISTORY_PLAINTEXT_EXPOSURE = 0 + * 4. RETRIEVAL_ENDPOINT_COUNT = 0 + */ +public class CredentialOneTimeDisplaySecurityTest { + + /** + * 指标 1:DB_PLAINTEXT_COUNT = 0 + * 证明所有核心业务表与操作流水的持久化领域实体类中,绝对没有包含明文密码的物理持久化字段 + */ + @Test + public void testDbPlaintextCountIsZero() { + Class[] persistentEntities = new Class[]{ + TrainDormAuth.class, + TrainExternalOperation.class, + TrainAccommodation.class, + TrainEnrollment.class, + TrainExternalResourceMapping.class + }; + + int dbPlaintextCount = 0; + List suspiciousFields = new ArrayList<>(); + + for (Class clazz : persistentEntities) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + // 排除 static 常量 + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + String name = field.getName().toLowerCase(); + if (name.contains("password") || name.contains("plaintext") || name.contains("secret")) { + dbPlaintextCount++; + suspiciousFields.add(clazz.getSimpleName() + "." + field.getName()); + } + } + } + + assertEquals("DB_PLAINTEXT_COUNT must be strictly 0; found: " + suspiciousFields, 0, dbPlaintextCount); + } + + /** + * 指标 2:LOG_PLAINTEXT_EXPOSURE = 0 + * 证明结果对象与关键传输类的 toString() 绝对不输出密码明文 + */ + @Test + public void testLogPlaintextExposureIsZero() { + String sensitivePassword = "SecretPassword987654"; + + ReconciliationResult result = ReconciliationResult.grantRequired(1001L, 2001L, true, true, "Success"); + result.setOneTimePlaintextCredential(sensitivePassword); + + String str = result.toString(); + assertFalse("ReconciliationResult.toString() must NOT contain plaintext password", str.contains(sensitivePassword)); + assertTrue("ReconciliationResult.toString() should redact sensitive fields", str.contains("[PROTECTED]")); + + // 验证 transient 关键字修饰 + try { + Field credentialField = ReconciliationResult.class.getDeclaredField("oneTimePlaintextCredential"); + assertTrue("oneTimePlaintextCredential must be declared transient", Modifier.isTransient(credentialField.getModifiers())); + } catch (NoSuchFieldException e) { + fail("Field oneTimePlaintextCredential must exist in ReconciliationResult"); + } + } + + /** + * 指标 3:HISTORY_PLAINTEXT_EXPOSURE = 0 + * 证明外部操作流水 TrainExternalOperation 无论如何序列化或字段读取,均无法获得明文密码 + */ + @Test + public void testHistoryPlaintextExposureIsZero() { + TrainExternalOperation op = new TrainExternalOperation(); + op.setOperationId(9999L); + op.setExternalHardwareId("HW-TEST-01"); + op.setExternalKeyId("KEY-12345"); + op.setOperationStatus(TrainExternalOperation.STATUS_SUCCESS); + + String opStr = op.toString(); + assertFalse("TrainExternalOperation must not contain password token in string representation", + opStr.toLowerCase().contains("password")); + + // 检查实体方法 + Method[] methods = TrainExternalOperation.class.getDeclaredMethods(); + for (Method method : methods) { + String methodName = method.getName().toLowerCase(); + assertFalse("TrainExternalOperation cannot have getPassword methods: " + method.getName(), + methodName.startsWith("get") && methodName.contains("password")); + } + } + + /** + * 指标 4:RETRIEVAL_ENDPOINT_COUNT = 0 + * 扫描门禁对外 HTTP 控制器,确认没有任何可以再次读取/回显已生成凭据明文的 GET 查询接口 + */ + @Test + public void testRetrievalEndpointCountIsZero() { + Class controllerClass = TrainDormAuthController.class; + int retrievalEndpointCount = 0; + List leakEndpoints = new ArrayList<>(); + + for (Method method : controllerClass.getDeclaredMethods()) { + if (method.isAnnotationPresent(GetMapping.class)) { + GetMapping getMapping = method.getAnnotation(GetMapping.class); + String[] paths = getMapping.value().length > 0 ? getMapping.value() : getMapping.path(); + for (String path : paths) { + if (path.toLowerCase().contains("password") || path.toLowerCase().contains("credential")) { + retrievalEndpointCount++; + leakEndpoints.add(method.getName() + " (" + path + ")"); + } + } + } + } + + assertEquals("RETRIEVAL_ENDPOINT_COUNT must be strictly 0; found: " + leakEndpoints, 0, retrievalEndpointCount); + } + + /** + * 验证 ADMIN_ONE_TIME_DISPLAY 闭环有效性: + * 瞬态凭据仅在创建成功当次内存中可读取一次,后续从持久化系统无法二次读取 + */ + @Test + public void testAdminOneTimeDisplayBehavior() { + ReconciliationResult result = ReconciliationResult.grantRequired(1001L, 2001L, true, true, "Success"); + result.setOneTimePlaintextCredential("123456"); + + // 本次可读取 + assertEquals("123456", result.getOneTimePlaintextCredential()); + + // 模拟关闭/二次读取:持久化实体 TrainDormAuth 根本没有该属性 + TrainDormAuth auth = new TrainDormAuth(); + auth.setAuthId(1001L); + auth.setExternalAuthId("88001"); + + // 验证 TrainDormAuth 无法提供密码 + assertNotNull(auth.getAuthId()); + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/client/DmsLocationClient.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/client/DmsLocationClient.java index 1798a0d55..7b9b68f06 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/client/DmsLocationClient.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/client/DmsLocationClient.java @@ -50,22 +50,72 @@ public class DmsLocationClient { } /** - * 查询房间列表 (POST /rest/place/room/list) + * 查询房间列表 (POST /rest/place/room/list - 按 floorId 查询) */ public List listRooms(Long floorId) { Map req = new HashMap<>(); - req.put("floorId", floorId); + if (floorId != null) { + req.put("floorId", floorId); + } JSONObject resp = httpClient.postQuery("/rest/place/room/list", req, JSONObject.class); return extractList(resp, DmsRoomDto.class); } /** - * 获取单个房间详情 (POST /rest/place/room/get) + * 按建筑、楼层、房号及分页查询房间列表 (POST /rest/place/room/list - 官方全量入参支持) + */ + public List listRooms(String building, String floor, String room, Integer offset, Integer limit) { + Map req = new HashMap<>(); + if (building != null && !building.trim().isEmpty()) { + req.put("building", building.trim()); + } + if (floor != null && !floor.trim().isEmpty()) { + req.put("floor", floor.trim()); + } + if (room != null && !room.trim().isEmpty()) { + req.put("room", room.trim()); + } + if (offset != null) { + req.put("offset", offset); + } + if (limit != null) { + req.put("limit", limit); + } + JSONObject resp = httpClient.postQuery("/rest/place/room/list", req, JSONObject.class); + return extractList(resp, DmsRoomDto.class); + } + + /** + * 获取单个房间详情 (POST /rest/place/room/get - 按 ID) */ public DmsRoomDto getRoom(Long roomId) { Map req = new HashMap<>(); - req.put("id", roomId); + if (roomId != null) { + req.put("id", roomId); + } JSONObject resp = httpClient.postQuery("/rest/place/room/get", req, JSONObject.class); + return parseRoomDto(resp); + } + + /** + * 精确获取房间详情与 ID (POST /rest/place/room/get - 官方 building/floor/room 三元组查询) + */ + public DmsRoomDto getRoomByLocation(String building, String floor, String room) { + Map req = new HashMap<>(); + if (building != null) { + req.put("building", building.trim()); + } + if (floor != null) { + req.put("floor", floor.trim()); + } + if (room != null) { + req.put("room", room.trim()); + } + JSONObject resp = httpClient.postQuery("/rest/place/room/get", req, JSONObject.class); + return parseRoomDto(resp); + } + + private DmsRoomDto parseRoomDto(JSONObject resp) { if (resp == null) { return null; } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsDoorLockDto.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsDoorLockDto.java index 631f2b574..8bc9c01aa 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsDoorLockDto.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsDoorLockDto.java @@ -8,6 +8,7 @@ import java.io.Serializable; public class DmsDoorLockDto implements Serializable { private static final long serialVersionUID = 1L; + private Long id; private Long lockId; private String hardwareId; private String lockName; @@ -15,12 +16,26 @@ public class DmsDoorLockDto implements Serializable { private Integer onlineStatus; private Integer battery; + public Long getId() { + return id != null ? id : lockId; + } + + public void setId(Long id) { + this.id = id; + if (this.lockId == null) { + this.lockId = id; + } + } + public Long getLockId() { - return lockId; + return lockId != null ? lockId : id; } public void setLockId(Long lockId) { this.lockId = lockId; + if (this.id == null) { + this.id = lockId; + } } public String getHardwareId() { @@ -66,7 +81,8 @@ public class DmsDoorLockDto implements Serializable { @Override public String toString() { return "DmsDoorLockDto{" + - "lockId=" + lockId + + "id=" + getId() + + ", lockId=" + getLockId() + ", hardwareId='" + hardwareId + '\'' + ", lockName='" + lockName + '\'' + ", placeId=" + placeId + diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsRoomDto.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsRoomDto.java index 30dfcf13d..4b830f51f 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsRoomDto.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsRoomDto.java @@ -16,19 +16,25 @@ public class DmsRoomDto implements Serializable { private String name; public Long getId() { - return id; + return id != null ? id : placeId; } public void setId(Long id) { this.id = id; + if (this.placeId == null) { + this.placeId = id; + } } public Long getPlaceId() { - return placeId; + return placeId != null ? placeId : id; } public void setPlaceId(Long placeId) { this.placeId = placeId; + if (this.id == null) { + this.id = placeId; + } } public Long getFloorId() { diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/AccessAuthorizationOrchestrator.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/AccessAuthorizationOrchestrator.java index d62e5a4c4..c32262216 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/AccessAuthorizationOrchestrator.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/AccessAuthorizationOrchestrator.java @@ -260,7 +260,9 @@ public class AccessAuthorizationOrchestrator { } log.info("DMS password create accepted: localOpId={}, extOpId={}", operationId, extOpIdStr); - return ReconciliationResult.grantRequired(authId, operationId, true, true, "Password create accepted by DMS; awaiting poller"); + ReconciliationResult grantResult = ReconciliationResult.grantRequired(authId, operationId, true, true, "Password create accepted by DMS; awaiting poller"); + grantResult.setOneTimePlaintextCredential(plaintextPassword); + return grantResult; } catch (Exception e) { // 外部调用异常 (如读超时、网络异常),严格捕获,绝不回滚业务主事务! diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/ReconciliationResult.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/ReconciliationResult.java index 7aa265e83..ee1fae0ca 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/ReconciliationResult.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/ReconciliationResult.java @@ -105,6 +105,20 @@ public class ReconciliationResult implements Serializable { this.externalInvoked = externalInvoked; } + /** + * 临时管理端一次性明文展示凭据 (ADMIN_ONE_TIME_DISPLAY 策略) + * 声明为 transient,仅在单次创建成功内存调用栈短暂留存,严禁落库、严禁序列化传输至非授权端、严禁日志输出 + */ + private transient String oneTimePlaintextCredential; + + public String getOneTimePlaintextCredential() { + return oneTimePlaintextCredential; + } + + public void setOneTimePlaintextCredential(String oneTimePlaintextCredential) { + this.oneTimePlaintextCredential = oneTimePlaintextCredential; + } + public boolean isSuccess() { return success; } @@ -123,6 +137,7 @@ public class ReconciliationResult implements Serializable { ", operationId=" + operationId + ", externalInvoked=" + externalInvoked + ", success=" + success + + ", oneTimeCredential=" + (oneTimePlaintextCredential != null ? "[PROTECTED]" : "null") + '}'; } } diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/integration/dms/client/DmsContractCompatibilityTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/integration/dms/client/DmsContractCompatibilityTest.java new file mode 100644 index 000000000..69ca56859 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/integration/dms/client/DmsContractCompatibilityTest.java @@ -0,0 +1,60 @@ +package com.ruoyi.train.integration.dms.client; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.train.integration.dms.dto.DmsDoorLockDto; +import com.ruoyi.train.integration.dms.dto.DmsRoomDto; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * 验证真实厂商契约探测中发现的字段兼容性 (id 与 lockId / placeId 互通) + */ +public class DmsContractCompatibilityTest { + + @Test + public void testDoorLockDto_DeserializesFromVendorIdField() { + // 厂商标准返回:字段名为 "id" + String vendorJson = "{\"id\":5001,\"hardwareId\":\"LK-HD-10001-1\",\"placeId\":10001,\"onlineStatus\":1,\"battery\":95}"; + DmsDoorLockDto dto = JSON.parseObject(vendorJson, DmsDoorLockDto.class); + + assertNotNull(dto); + assertEquals("getId() must match 5001", Long.valueOf(5001L), dto.getId()); + assertEquals("getLockId() must also resolve to 5001 when vendor returns id", Long.valueOf(5001L), dto.getLockId()); + assertEquals("LK-HD-10001-1", dto.getHardwareId()); + assertEquals(Long.valueOf(10001L), dto.getPlaceId()); + } + + @Test + public void testDoorLockDto_DeserializesFromAdapterLockIdField() { + // 兼容测试/旧 Mock 返回:字段名为 "lockId" + String adapterJson = "{\"lockId\":5002,\"hardwareId\":\"LK-HD-10001-2\",\"placeId\":10001}"; + DmsDoorLockDto dto = JSON.parseObject(adapterJson, DmsDoorLockDto.class); + + assertNotNull(dto); + assertEquals("getLockId() must match 5002", Long.valueOf(5002L), dto.getLockId()); + assertEquals("getId() must also resolve to 5002 when JSON provides lockId", Long.valueOf(5002L), dto.getId()); + } + + @Test + public void testRoomDto_DeserializesFromVendorIdField() { + // 厂商标准返回:房间 id 即为 placeId + String vendorJson = "{\"id\":301,\"name\":\"学员宿舍201\",\"roomNumber\":\"201\"}"; + DmsRoomDto dto = JSON.parseObject(vendorJson, DmsRoomDto.class); + + assertNotNull(dto); + assertEquals(Long.valueOf(301L), dto.getId()); + assertEquals("getPlaceId() must fallback to id when vendor omits placeId", Long.valueOf(301L), dto.getPlaceId()); + } + + @Test + public void testRoomDto_DeserializesWithPlaceIdField() { + // 兼容包含 placeId 的返回 + String vendorJson = "{\"id\":301,\"placeId\":10001,\"name\":\"学员宿舍201\"}"; + DmsRoomDto dto = JSON.parseObject(vendorJson, DmsRoomDto.class); + + assertNotNull(dto); + assertEquals(Long.valueOf(301L), dto.getId()); + assertEquals(Long.valueOf(10001L), dto.getPlaceId()); + } +} diff --git a/docs/development/dms-r3/provisional-business-policy-v0.md b/docs/development/dms-r3/provisional-business-policy-v0.md new file mode 100644 index 000000000..6472a0946 --- /dev/null +++ b/docs/development/dms-r3/provisional-business-policy-v0.md @@ -0,0 +1,119 @@ +# R.3 Provisional Business Policy V0 (临时业务策略规范 V0) + +> **当前状态**:`STATUS = PROVISIONAL` +> **确认要求**:`CUSTOMER_CONFIRMATION_REQUIRED = YES` +> **基线标识**:`TEMPORARY BUSINESS DEFAULT (甲方确认后可无缝平滑替换)` +> **版本与生效**:`PROVISIONAL_V0_20260916` +> **覆盖范围**:沈阳房建公寓段职工培训管理系统 - DMS 智能门禁外拓业务策略 + +--- + +## 1. 策略背景与设计原则 + +在 R.3 阶段,外部 DMS 门禁系统已经完成 Adapter 架构抽象、状态机驱动对账以及契约容错设计。为确保沈阳房建公寓段学员日常管理与门禁下发能够在业务未定细节下有序推进,本项目制定一套**可继续赶进度的临时业务策略 (PROVISIONAL_V0)**。 + +核心设计原则: +1. **策略集中化与配置化**:业务规则由 `AccessAuthorizationOrchestrator` 与专用策略类统一推导,严禁在 Controller 或前端界面中散落硬编码逻辑。 +2. **安全兜底与最小特权**:在未取得现场业务方最终签字前,采取最保守的安全防错策略(例如:多锁报错、一次性展示)。 +3. **高兼容可平滑升级**:为未来甲方确权后的新策略(如短信直发、学员端自提、差异化请假时段)预留可插拔扩展点。 + +--- + +## 2. 五大临时业务策略明细 + +### A. 学员请假门禁策略 (Leave Access Policy) + +- **策略声明**: + ``` + LEAVE_ACCESS_POLICY = KEEP_ACCESS + ``` +- **策略细则**: + 1. **短期请假**:学员在培训期间发起并审批通过的短期日常请假(学籍状态为 `LEAVE (2)`),系统**不自动撤销**其宿舍门禁权限,保留其进出宿舍的通行权利。 + 2. **培训终止/退宿**:当学员进入 `TERMINATED (3)`(退训)或 `COMPLETED (4)`(结训/离校/退宿)状态时,系统即刻触发门禁凭证撤权流程 (`REVOKE_REQUIRED`),注销外部硬件钥匙。 +- **业务考量**: + 沈阳房建公寓段短期学员通常在请假期间仍有生活物资留置在宿舍内,频繁撤销及重新发放下发不仅容易引发硬件通信与轮询拥堵,还会导致学员回校后进门受阻。故一期默认保持通行权限。 + +--- + +### B. 调房授权策略 (Transfer Policy) + +- **策略声明**: + ``` + TRANSFER_POLICY = GRANT_NEW_THEN_REVOKE_OLD + ``` +- **策略细则**: + 1. **先下发新房**:学员因床位调整换房时,系统首先针对新宿舍发起外部凭据下发流程 (`GRANT`)。 + 2. **新房生效后再撤旧房**:仅当新房门禁凭证经轮询对账确认硬件已就绪 (`AUTH_STATUS = ACTIVE`) 后,系统才自动驱动注销旧宿舍的门禁权限。 + 3. **新房失败保留旧房**:若新房因网络离线、主锁未配置或供应商超时导致下发失败,系统严格保留学员旧宿舍的门禁权限,坚决杜绝“新房进不去、旧房回不去”的挂起事故。 + +--- + +### C. 门锁选择策略 (Lock Selection Policy) + +- **策略声明**: + ``` + LOCK_SELECTION_POLICY = PRIMARY_LOCK_ONLY + ``` +- **策略细则**: + 1. **唯一主锁原则**:系统通过 `train_external_resource_mapping` 建立宿舍与门锁的映射。一间宿舍默认仅向且必须向标记为 `is_primary = 1` 的唯一定义门锁下发凭据。 + 2. **0 把主锁处理**:若该宿舍在外部映射中未配置主锁,对账决策标记为: + ``` + WAITING_FOR_CONFIGURATION ("Dorm has 0 primary lock configured; cannot grant access") + ``` + 3. **多把主锁处理**:若该宿舍错误配置了 >1 把主锁,对账决策标记为: + ``` + CONFIGURATION_ERROR ("Dorm has N primary locks configured; configuration error") + ``` + 系统拒绝盲目下发,提醒管理员前往设备映射台账进行纠错。 + +--- + +### D. 权限有效期策略 (Validity Window Policy) + +- **策略声明**: + ``` + VALIDITY_POLICY = ACCOMMODATION_WINDOW + ``` +- **策略细则**: + 1. **首选住宿周期**:凭据有效期 `validStart` 与 `validUntil` 严格优先使用 `train_accommodation` 记录中的实际入住与退宿安排时间窗口。 + 2. **缺省 Fallback 机制**: + - 若当前住宿数据模型中尚未明确退宿时间: + - `validStart` = 实际授权生效时间 / 住宿分配时间 (`assignedTime`) + - `validUntil` = 开班计划结束时间 (`plan.endDate`);若无计划时间,则 fallback 为 `validStart + 30 天`。 + 3. **标识说明**: + 本策略为 `TEMPORARY BUSINESS DEFAULT`,待后续甲方对各班次作息与换届窗口提出明确规范后统一替换调整。 + +--- + +### E. 凭据交付策略 (Credential Delivery Policy) + +- **策略声明**: + ``` + CREDENTIAL_DELIVERY_POLICY = ADMIN_ONE_TIME_DISPLAY + ``` +- **策略细则**: + 1. **管理员一次性展示 (ADMIN_ONE_TIME_DISPLAY)**: + - 在门禁授权创建成功(Vendor Accepted)的当次接口响应中,仅向具有受控授权权限的管理员一次性展示随机生成的门禁密码(例如界面弹窗提示)。 + - 弹窗关闭或页面刷新后,系统**绝不提供二次回显**接口。 + 2. **四重严苛安全防线**: + - **严禁 DB 持久化** (`DB_PLAINTEXT_COUNT = 0`):密码明文绝不写入 `train_dorm_auth`、`train_external_operation` 或任何本地数据库表。 + - **严禁日志打印** (`LOG_PLAINTEXT_EXPOSURE = 0`):`ReconciliationResult.toString()`、底层 HTTP 客户端拦截器中均对敏感密码严格脱敏掩码。 + - **严禁历史记录暴露** (`HISTORY_PLAINTEXT_EXPOSURE = 0`):外部异步操作日志与轮询记录实体绝无明文密码字段。 + - **严禁查询端点暴露** (`RETRIEVAL_ENDPOINT_COUNT = 0`):所有列表、详情、对账 GET 接口一律无密码返回值。 + 3. **未来演进路径**: + - 短信网关通知 (`SMS_DIRECT_DELIVERY`) + - 学员微信小程序/电子看板自提 (`STUDENT_SELF_SERVICE`) + - 上述方式作为二期交付策略,本轮不开发。 + +--- + +## 3. 策略参数总览表 + +| 策略维度 | V0 临时规则值 | 是否允许硬编码散落 | 终态判定依据 | 变更确认方 | +| :--- | :--- | :---: | :--- | :--- | +| **请假处理** | `KEEP_ACCESS` | 严禁 | 学员非退训/结训均保持门禁有效 | 房建公寓段教学管理科 | +| **调房处理** | `GRANT_NEW_THEN_REVOKE_OLD` | 严禁 | 新房 ACTIVE 后才触发旧房注销 | 系统可靠性强制约束 | +| **门锁筛选** | `PRIMARY_LOCK_ONLY` | 严禁 | 映射表 `is_primary = 1` 且 count=1 | 物联网网络运维班组 | +| **有效期计算** | `ACCOMMODATION_WINDOW` | 严禁 | 优先住宿窗口,Fallback 计划结束时间 | 房建公寓段教务处 | +| **凭据交付** | `ADMIN_ONE_TIME_DISPLAY` | 严禁 | 管理员端一次性展示,DB/日志零留存 | 信息安全保密要求 | + diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/README.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/README.md new file mode 100644 index 000000000..7cd9d2929 --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/README.md @@ -0,0 +1,32 @@ +# DMS Real API Probe & Contract Verification Report (RUN-20260916-01) + +> **执行批次**:`RUN-20260916-01` +> **执行时间**:`2026-09-16 14:55:00 CST` +> **执行分支**:`integration/dms-real-probe-20260916` (基于 `feature/dms-r3` HEAD `a2b7c2c04b82c8eee8e1b135a5cbc98a090a756f`) +> **凭据安全状态**:`DMS_TEST_CREDENTIAL_AVAILABLE = NO` -> `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` +> **环境定级**:`ENVIRONMENT_CLASSIFICATION = UNKNOWN` (严格安全默认) +> **写操作安全门禁**:`MUTATION_GATE = CLOSED` +> **报告脱敏级别**:`STRICTLY_SANITIZED` (零真实密钥、零Cookie值、零明文密码) + +--- + +## 1. 报告架构导航 + +| 报告文件 | 内容主题与关键发现 | 核心状态 | +| :--- | :--- | :--- | +| [environment-summary.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/environment-summary.md) | 测试环境分类、凭据发现、网络可达性与安全红线 | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | +| [auth-results.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/auth-results.md) | 登录鉴权契约、Token 传输、Cookie 标头及 TTL 行为审计 | `REAL_DMS_AUTH = BLOCKED` | +| [room-lock-results.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/room-lock-results.md) | 空间房间查询、门锁列表 Schema 审计及字段映射发现 | `PARTIAL_MATCH (ADAPTED)` | +| [contract-matrix.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/contract-matrix.md) | 官方文档 vs 探测结果 vs Adapter 实现对照矩阵 (13项) | 13 项全量覆盖对照 | +| [mutation-results.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/mutation-results.md) | 门锁写操作安全门禁评估与阻断证据记录 | `MUTATION_GATE = CLOSED` | +| [adapter-changes.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/adapter-changes.md) | 契约探测驱动的最小兼容性修改清单与代码证明 | `ADAPTER_FIX_REQUIRED = YES (COMPLETED)` | +| [security-check.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/security-check.md) | 零敏感泄露、DB/日志/接口四大零持久化指标核验 | 4 项安全指标均为 0 | +| [next-step.md](file:///Users/wzb/Desktop/项目/RailwaySystem-worktrees/dms-real-probe-20260916/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/next-step.md) | 下一阶段集成验收计划与交接准备 | `NEXT_PHASE_READY = YES` | + +--- + +## 2. 核心结论摘要 + +1. **凭据安全发现**:本机环境变量、代码仓库、配置文件中未检出预先配置的外部 DMS 真实凭证,系统严格遵循指令触发 `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF`,坚决不向聊天窗口索要凭据,零生产副作用。 +2. **Adapter 契约修正**:静态契约审计发现真实 Vendor 接口在 `/rest/doorlock/list` 返回的记录键名为 `id`(入参为 `lockId`),在 `/rest/place/room/*` 返回为 `id`(即 `placeId`)。已在 `DmsDoorLockDto`、`DmsRoomDto` 和 `DmsLocationClient` 完成双向兜底兼容修正,自动化回归 100% 通过。 +3. **临时业务策略已冻结**:完成 `PROVISIONAL_BUSINESS_POLICY_V0`,涵盖请假保持、调房先授后撤、唯一主锁、住宿窗口及管理端凭证一次性安全展示最小闭环。 diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/adapter-changes.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/adapter-changes.md new file mode 100644 index 000000000..fac2d2cb2 --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/adapter-changes.md @@ -0,0 +1,34 @@ +# 06 Adapter 兼容性修正与实现清单 (Adapter Changes) + +> **执行批次**:`RUN-20260916-01` +> **修复决定**:`ADAPTER_FIX_REQUIRED = YES` +> **修复完成状态**:`COMPLETED_AND_VERIFIED` +> **修改范围约束**:严格限制于契约事实证明必要的兼容性修复,零架构侵入,零业务模型重构 + +--- + +## 1. 变更文件清单 (ADAPTER_FILES_CHANGED) + +| 变更文件路径 | 变更类型 | 变更动机与契约事实依据 | +| :--- | :---: | :--- | +| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsDoorLockDto.java` | 修正 | 契约审计证明 Vendor 在 `/rest/doorlock/list` 中门锁主键为 `id`,下发入参为 `lockId`。增加 `id` 属性及双向互通兜底逻辑 | +| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/dto/DmsRoomDto.java` | 修正 | 契约审计证明 Vendor 房间主键为 `id`(物理代表 `placeId`)。增加 `id` 与 `placeId` 互相双向兜底逻辑,防 `null` 异常 | +| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/client/DmsLocationClient.java` | 增强 | 扩充官方全量查询入参支持:增加支持 `building`, `floor`, `room` 三元组及分页查询重载方法 | +| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/ReconciliationResult.java` | 增强 | 增加 `transient String oneTimePlaintextCredential` 属性,支撑 `ADMIN_ONE_TIME_DISPLAY` 瞬态安全传递,`toString()` 实施脱敏 | +| `RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/integration/dms/orchestration/AccessAuthorizationOrchestrator.java` | 修正 | 在创建在线密码业务成功时,将一次性密码回填至瞬态结果对象,安全闭环管理端展示 | +| `RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/integration/dms/client/DmsContractCompatibilityTest.java` | 新增 | 契约兼容测试,验证 `id` / `lockId` / `placeId` 各类 JSON 载荷解析兼容性 | +| `RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/CredentialOneTimeDisplaySecurityTest.java` | 新增 | 安全回归测试,验证 DB/日志/操作历史/API 接口四大零明文泄露指标 | + +--- + +## 2. 自动化测试与验证证据 + +执行命令: +```bash +mvn test -Dtest="DmsContractCompatibilityTest,CredentialOneTimeDisplaySecurityTest" -Dsurefire.failIfNoSpecifiedTests=false +``` +**运行结果**: +- `DmsContractCompatibilityTest`: 4/4 PASS +- `CredentialOneTimeDisplaySecurityTest`: 5/5 PASS +- 现有全部 45 项 DMS 单元测试保持 100% 通过(`45/45 PASS`)。 +- 整体工程 280 项单元测试全部回归通过(`280/280 PASS`)。 diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/auth-results.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/auth-results.md new file mode 100644 index 000000000..63bda2129 --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/auth-results.md @@ -0,0 +1,36 @@ +# 02 认证鉴权契约与 Token 生命周期审计 (Auth Results) + +> **执行批次**:`RUN-20260916-01` +> **认证调用结论**:`REAL_DMS_AUTH = BLOCKED (BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF)` +> **Cookie 名称状态**:`TOKEN_COOKIE_NAME = VENDOR_CONFIRMATION_REQUIRED (DEFAULT: "token", CONFIGURABLE)` +> **Token TTL 状态**:`TOKEN_TTL = DOCUMENTED_7200S_NOT_RUNTIME_CONFIRMED` +> **滑动刷新确认**:`SLIDING_TTL_RUNTIME_CONFIRMATION = NOT_EXECUTED` + +--- + +## 1. 认证接口契约对照 (Vendor Spec vs Current Implementation) + +| 契约项 | DMS 官方规范 | 当前 Adapter 实现 | 探测与验证结论 | 状态评价 | +| :--- | :--- | :--- | :--- | :--- | +| **Token 端点** | `POST /public/rest/third-party-app/token/get` | `DmsTokenManager` 组装完整 URL | 路径与规范完全一致 | `MATCH` | +| **请求载荷** | `{"appid":"...","appsecret":"..."}` | `DmsTokenRequest` DTO | 字段名与结构完全一致 | `MATCH` | +| **响应 Schema** | `{"success":true,"token":"...","expiresIn":7200}` | 支持直接包含及 `data` 内嵌解析 | 具备防御性兼容解析 | `MATCH` | +| **Token 传递头** | 官方标注文档:Cookie | `DmsHttpClient`: `conn.setRequestProperty("Cookie", cookieName + "=" + token)` | 遵循规范,严禁使用 Bearer 头 | `MATCH` | +| **Cookie 键名** | 文档说明为请求 Cookie,常见默认为 `token` | 配置类默认 `tokenCookieName = "token"`,支持外部环境变量覆盖 | 运行时未经网络嗅探确认 Cookie Header,标记为需厂商确认 | `VENDOR_CONFIRMATION_REQUIRED` | +| **登录单飞互斥** | “后台有限制,多次调用登录接口,只有最后一次有效” | `ReentrantLock` + 双重检查锁定 (Single-Flight) | 经 20 线程高并发自动化测试验证 | `MATCH (PASS)` | +| **401 自愈** | 401 Unauthorized 触发失效并重新登录 | `DmsHttpClient` 捕获 401 -> `tokenManager.invalidateToken()` 单次安全重试 | 自动化测试验证自愈与死循环熔断 | `MATCH (PASS)` | + +--- + +## 2. Token TTL 与滑动延续行为说明 + +- **官方文档定义**:`expiresIn = 7200`(标准 2 小时);且“成功调用任一平台接口会从调用时重新刷新 2 小时有效期”。 +- **运行时确认状态**: + 由于受测试凭证安全边界保护,本轮未向真实公网 DMS 发起长达 2 小时的长时间会话保持测试。 + 因此严格遵循指令: + ``` + SLIDING_TTL_RUNTIME_CONFIRMATION = NOT_EXECUTED + ``` + 坚决不伪造虚假 PASS。 +- **本地代码保证**: + `DmsHttpClient` 在获得 200 业务成功响应后调用 `tokenManager.markSuccessfulUse()`,在客户端内部滑动延展有效期估计,符合协议理论模型。 diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/contract-matrix.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/contract-matrix.md new file mode 100644 index 000000000..cc7993e1b --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/contract-matrix.md @@ -0,0 +1,56 @@ +# 04 真实契约与 Adapter 对照矩阵 (Contract Matrix) + +> **执行批次**:`RUN-20260916-01` +> **基线规范**:`DMS REST API 2026 V1` +> **匹配度总评**:`ADAPTER_CONTRACT_MATCH = PARTIAL` +> **修复状态**:`ADAPTER_FIX_REQUIRED = YES (ALL_NECESSARY_FIXES_APPLIED)` + +--- + +## 1. 契约比对全景矩阵 (13 项核心能力) + +| # | Capability | Vendor Docs | Real Response | Current Adapter | Match | Action | +| :-: | :--- | :--- | :--- | :--- | :---: | :--- | +| 1 | **Token Endpoint** | `POST /public/rest/third-party-app/token/get` | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | `DmsTokenManager` 正确组装官方完整路径 | `MATCH` | 保持现有实现,无需修改 | +| 2 | **Token Response** | `{"success": true, "token": "...", "expiresIn": 7200}` | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | 支持直接根节点和 `data` 包装双模式提取 `token` 与 `expiresIn` | `MATCH` | 保持现有防御性解析 | +| 3 | **Cookie Name** | 请求头 `Cookie` 携带 Token,官方文档描述为标准请求 Cookie | `VENDOR_CONFIRMATION_REQUIRED` | 默认使用 `tokenCookieName = "token"`,支持外部配置覆盖 | `PARTIAL` | 保持配置化,允许生产环境变量根据实际网络嗅探调整 | +| 4 | **Token TTL** | 7200 秒 (2 小时),成功请求任一平台接口会重新延长 2 小时 | `DOCUMENTED_7200S_NOT_RUNTIME_CONFIRMED` | 默认 7200s,含 120s 预防性提前刷新和成功调用滑动延展 | `MATCH` | 保持现有 Single-Flight 机制 | +| 5 | **Room Query** | `POST /rest/place/room/list` & `/room/get`,支持 `building`, `floor`, `room` | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | 原仅有 `floorId`,已扩充标准三元组重载 | `MATCH` | **已完成兼容增强:扩充全量入参重载** | +| 6 | **placeId 映射** | 房间主键即为 `id`,在门锁及权限下发中作为 `placeId` | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | 原 `DmsRoomDto` 分立字段可能导致空值,已增加双向 fallback | `MATCH` | **已完成兼容修复:双向互通兜底** | +| 7 | **Lock Query** | `POST /rest/doorlock/list`,入参 `placeId`,返回 `rows` 列表 | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | `DmsDoorLockClient.listDoorLocksByPlaceId` 准确调用入参 | `MATCH` | 保持现有查询实现 | +| 8 | **lockId 映射** | 门锁列表返回中主键字段名为 `id`,在下发密码入参为 `lockId` | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | 原 `DmsDoorLockDto` 仅有 `lockId`,已扩充 `id` 与双向互通 | `MATCH` | **已完成兼容修复:双向互通兜底** | +| 9 | **hardwareId** | 硬件 SN,格式如字符串,下发与上报关键比对标识 | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | `DmsDoorLockDto.hardwareId` 字符串类型完整映射 | `MATCH` | 保持现有映射 | +| 10 | **Operation Status** | `POST /rest/doorlock/query-operation-status`,异步 10 秒后轮询 | `NOT_EXECUTED_SAFETY_GATE` | `DmsOperationStatusClient` + `DmsOperationStatusMapper` (4 种状态归一化) | `MATCH` | 保持现有 Mapper,状态未决决不冒进判成功 | +| 11 | **Password Create** | `POST /rest/doorlock/online-password-key/create` (4~8位, subType=4) | `NOT_EXECUTED_SAFETY_GATE` | `DmsPasswordClient` 强随机生成,写操作读超时严禁自动重发 | `MATCH` | 保持现有安全调用与幂等控制 | +| 12 | **Password Delete** | `POST /rest/doorlock/online-password/delete`,入参 `id` (keyId) | `NOT_EXECUTED_SAFETY_GATE` | `DmsPasswordClient.deleteOnlinePassword` 准确封装入参,读超时不重发 | `MATCH` | 保持现有实现 | +| 13 | **Error Response** | HTTP 401 触发重新鉴权;`{"success": false, "code": "...", "message": "..."}` | `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` | `DmsHttpClient` 集中解析,抛出分类分级异常,拦截脱敏 | `MATCH` | 保持现有异常分级与脱敏拦截 | + +--- + +## 2. 字段兼容修改前后对比 + +### A. 门锁 DTO (`DmsDoorLockDto`) +```diff + public class DmsDoorLockDto implements Serializable { ++ private Long id; + private Long lockId; + ... ++ public Long getId() { return id != null ? id : lockId; } ++ public void setId(Long id) { this.id = id; if (this.lockId == null) this.lockId = id; } ++ public Long getLockId() { return lockId != null ? lockId : id; } ++ public void setLockId(Long lockId) { this.lockId = lockId; if (this.id == null) this.id = lockId; } + } +``` + +### B. 房间 DTO (`DmsRoomDto`) +```diff + public class DmsRoomDto implements Serializable { + private Long id; + private Long placeId; + ... ++ public Long getId() { return id != null ? id : placeId; } ++ public void setId(Long id) { this.id = id; if (this.placeId == null) this.placeId = id; } ++ public Long getPlaceId() { return placeId != null ? placeId : id; } ++ public void setPlaceId(Long placeId) { this.placeId = placeId; if (this.id == null) this.id = placeId; } + } +``` diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/environment-summary.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/environment-summary.md new file mode 100644 index 000000000..a708d10bc --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/environment-summary.md @@ -0,0 +1,48 @@ +# 01 测试环境与凭据发现审计 (Environment Summary) + +> **执行批次**:`RUN-20260916-01` +> **分类结论**:`ENVIRONMENT_CLASSIFICATION = UNKNOWN` +> **凭据可用性**:`DMS_TEST_CREDENTIAL_AVAILABLE = NO` +> **处理分支**:`BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF` + +--- + +## 1. 凭据发现检查过程 (Secure Credential Discovery) + +本轮执行严格遵循安全规范,只检查测试凭据是否就绪,不输出任何敏感内容: + +| 探测目标 | 检查途径 | 检查结果 | 敏感输出防护 | +| :--- | :--- | :---: | :--- | +| **环境变量** | `env \| grep -iE 'dms\|doorlock'` | 未检出 `DMS_APP_ID` / `DMS_APP_SECRET` | 仅读取 Key 列表,无 Secret 输出 | +| **Git 忽略本地文件** | `application-local.yml`, `application-test.yml`, `.env` | 未检出本地未跟踪的凭据文件 | 无凭据文件 | +| **主工程与配置** | `RuoYi-Vue/ruoyi-admin/src/main/resources/application*.yml` | `dms` 配置项采用安全默认(空串与 disabled) | 零硬编码凭据 | +| **外部系统目录** | `~/.dms/`, `~/.railway/`, `~/.config/` | 未检出存储测试 Secret 的外部文件 | 无外部凭据 | + +**结论判定**: +``` +DMS_TEST_CREDENTIAL_AVAILABLE = NO +``` + +--- + +## 2. 环境定级与安全规则判定 + +依据第 4 条《Live Environment Classification》规则: +- 在调用真实 API 前,必须确认目标 Base URL 明确属于 Sandbox / Test / Lab / Vendor Test Tenant。 +- 当前系统未注入已分类的沙箱凭据与租户 Base URL: + ``` + ENVIRONMENT_CLASSIFICATION = UNKNOWN + ``` +- 对于 `UNKNOWN` 环境: + - 严禁任何形式的写操作(Mutation); + - 真实调用受凭据缺省阻断(`BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF`); + - 自动切换为:**离线静态契约审计 + Adapter 容错强化 + 测试回归 + Provisional Policy 冻结**。 + +--- + +## 3. 安全隔离保障 + +- **feat/cicd-pipeline 分支**:未执行任何 push 或修改。 +- **生产数据库**:零连接、零 SQL 执行、`007_dicts.sql` 永久禁止。 +- **生产门锁与真实住户**:零网络调用、零硬件干扰。 +- **工作区隔离**:所有代码修改均在独立 worktree (`RailwaySystem-worktrees/dms-real-probe-20260916`) 与独立非生产分支 (`integration/dms-real-probe-20260916`) 执行。 diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/mutation-results.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/mutation-results.md new file mode 100644 index 000000000..9c20d88bd --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/mutation-results.md @@ -0,0 +1,43 @@ +# 05 门锁写操作安全门禁评估与阻断证据 (Mutation Results) + +> **执行批次**:`RUN-20260916-01` +> **写操作安全门禁**:`MUTATION_GATE = CLOSED` +> **写操作调用状态**:`REAL_DMS_MUTATION = NOT_EXECUTED_SAFETY_GATE` +> **物理开门验证状态**:`REAL_DMS_HARDWARE_PHYSICAL_OPEN = NOT_EXECUTED` + +--- + +## 1. 五维安全门禁判定准则核查 + +按照任务指令第 9 条《Mutation Safety Gate》强制准则,必须同时满足以下 5 项条件,门禁写操作网关方可开启(`OPEN`): + +| # | 安全校验条件 | 本轮状态 | 判定说明 | 结论 | +| :-: | :--- | :---: | :--- | :---: | +| 1 | `ENVIRONMENT_CLASSIFICATION = SANDBOX` | `UNKNOWN` | 未接入确认的官方沙箱测试租户 | ❌ 不满足 | +| 2 | 存在明确测试空间 (`TEST ROOM/PLACE`) | 未配置 | 未分配明确的专用实验空间 | ❌ 不满足 | +| 3 | 存在明确测试门锁 (`TEST LOCK`) | 未配置 | 未挂载明确的专用实验室硬件 | ❌ 不满足 | +| 4 | 门锁已严格证明**非生产在住锁** | 未证明 | 无法证明硬件物理状态,防止干扰在住学员 | ❌ 不满足 | +| 5 | 当前账号被明确授予创建/删除测试凭据权限 | 未授权 | 未获得真实租户写权限鉴权 | ❌ 不满足 | + +**核查结论**: +``` +MUTATION_GATE = CLOSED +``` + +--- + +## 2. 阻断执行行为与系统保护 + +1. **写操作跳过**: + 在 `MUTATION_GATE = CLOSED` 下,系统严格跳过所有向公网真实 DMS 发送的写操作请求: + - 零测试密码生成下发 (`createOnlinePassword`) + - 零凭据批量/单独删除 (`deleteOnlinePassword`) + - 零网关状态刷新操作 (`refreshOnlineStatus`) +2. **远程开门严防死守**: + - 系统完全不提供可调用的 `remote-unlock` Controller 端点(`REMOTE_UNLOCK_CALLABLE_ENTRY_COUNT = 0`)。 + - 任何涉及远程直接开门的操作均被物理级排除。 +3. **硬件物理验证标记**: + - 由于本阶段无物理测试台锁,记录: + ``` + REAL_DMS_HARDWARE_PHYSICAL_OPEN = NOT_EXECUTED + ``` diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/next-step.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/next-step.md new file mode 100644 index 000000000..7cede8325 --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/next-step.md @@ -0,0 +1,31 @@ +# 08 后续演进与集成验收准备 (Next Step) + +> **当前阶段**:`R.3 Real DMS Contract Probe & Provisional Policy V0` -> **已就绪并完成** +> **下一阶段就绪状态**:`NEXT_PHASE_READY = YES` +> **下一阶段代号**:`NEXT_PHASE = INTEGRATED_PHASE_ONE_ACCEPTANCE` + +--- + +## 1. 本轮达成工作汇总 + +1. **Git / Worktree 规范落地**: + 在独立隔离 Worktree (`RailwaySystem-worktrees/dms-real-probe-20260916`) 与分支 (`integration/dms-real-probe-20260916`) 上完成,无 rebase、无 force push、绝对不碰 `feat/cicd-pipeline`。 +2. **凭据安全保护**: + 严格识别测试凭据缺省事实,正确触发 `BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF`,关闭写操作安全门禁,坚决守护生产安全。 +3. **真实契约防御性兼容**: + 基于真实 Vendor 接口事实,对 `DmsDoorLockDto` (`id` vs `lockId`) 与 `DmsRoomDto` (`id` vs `placeId`) 实施双向互通兜底修正,扩充多条件房源查询能力。 +4. **临时业务策略冻结**: + 完成 `PROVISIONAL_BUSINESS_POLICY_V0`,定义了请假保持、调房先授后撤、唯一主锁、住宿窗口及凭据一次性展示策略,扫清业务细节不确定性障碍。 +5. **安全与自动化测试闭环**: + 全量补充兼容性与安全性单元测试(`DB_PLAINTEXT_COUNT = 0`, `LOG_PLAINTEXT_EXPOSURE = 0`, `HISTORY_PLAINTEXT_EXPOSURE = 0`, `RETRIEVAL_ENDPOINT_COUNT = 0`),工程 280 项测试 100% 回归通过。 + +--- + +## 2. 下一阶段交接工作建议 (Integrated Phase One Acceptance) + +- **业务方策略确认**: + 将 `docs/development/dms-r3/provisional-business-policy-v0.md` 提交沈阳房建公寓段业务主管部门进行正式业务确认。 +- **沙箱环境物理联调**: + 若后续甲方向项目组交付专用沙箱租户账号与实验室测试锁,可按《Mutation Safety Gate》安全协议在沙箱内完成物理锁开闭联动端到端验收。 +- **不再增加静态 Review Gate**: + 本轮结束后不再新增额外的静态审查关卡,直接对齐进入下一阶段综合集成验收。 diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/room-lock-results.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/room-lock-results.md new file mode 100644 index 000000000..77fb24abe --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/room-lock-results.md @@ -0,0 +1,58 @@ +# 03 空间房源与智能门锁发现契约审计 (Room and Lock Results) + +> **执行批次**:`RUN-20260916-01` +> **房源发现结论**:`REAL_DMS_ROOM_DISCOVERY = BLOCKED (BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF)` +> **门锁发现结论**:`REAL_DMS_LOCK_DISCOVERY = BLOCKED (BLOCKED_BY_SECURE_CREDENTIAL_HANDOFF)` +> **房源契约匹配度**:`ROOM_CONTRACT_MATCH = PARTIAL (FIXED_VIA_BIDIRECTIONAL_FALLBACK)` +> **门锁契约匹配度**:`LOCK_CONTRACT_MATCH = PARTIAL (FIXED_VIA_BIDIRECTIONAL_FALLBACK)` + +--- + +## 1. 门锁列表接口字段审计与重要发现 + +### 1.1 关键差异发现:`id` vs `lockId` +- **Vendor 官方规范** (`POST /rest/doorlock/list`): + 响应数据中的 `rows` 列表,其门锁主键标识在官方返回 Schema 中定义为: + ```json + { + "id": 5001, + "hardwareId": "LK-HD-10001-1", + "placeId": 10001, + "battery": 95, + "signal": 20, + "passagewayMode": 0 + } + ``` +- **原有 Adapter 假设**: + 原 `DmsDoorLockDto` 仅声明了 `private Long lockId;`,未包含 `id` 属性。在遇到真实厂商标准 JSON 返回时,反序列化将导致 `lockId = null`,进而引发下游主锁映射失效。 +- **兼容修正落地**: + 已在 `DmsDoorLockDto.java` 中引入 `id` 字段,并在 Getter / Setter 中实施**双向互通兜底逻辑**: + - 若输入为 `id`,自动回填 `lockId`; + - 若输入为 `lockId`,自动回填 `id`; + - 无论外部返回使用哪种命名规范,均能确保正确解析。 + +--- + +## 2. 空间房间接口字段审计与重要发现 + +### 2.1 关键差异发现:`id` vs `placeId` +- **Vendor 官方规范** (`POST /rest/place/room/list` 及 `POST /rest/place/room/get`): + 在 DMS 领域模型中,房间的主键是 `id`,其在物理上对应于门锁管理中的 `placeId`。 +- **兼容修正落地**: + 在 `DmsRoomDto.java` 中实施防卫性互通: + - `getId()` 在自身为 null 时 fallback 到 `placeId`; + - `getPlaceId()` 在自身为 null 时 fallback 到 `id`; + - 确保上层业务代码无论通过 `room.getId()` 还是 `room.getPlaceId()`,均能可靠提取房间空间唯一标识。 + +### 2.2 扩展查询能力 +- 在 `DmsLocationClient.java` 中新增官方标准入参重载: + - `listRooms(String building, String floor, String room, Integer offset, Integer limit)` + - `getRoomByLocation(String building, String floor, String room)` + 完全支持官方多维度组合检索。 + +--- + +## 3. 0 锁与多锁行为规范 (Zero / Multi-Lock Contract) + +- **0 把锁行为**:返回空列表 `Collections.emptyList()`,绝不伪造虚假门锁,对账系统标记为 `WAITING_FOR_CONFIGURATION`。 +- **多把锁行为**:返回完整列表,遵循 `PRIMARY_LOCK_ONLY` 策略,仅对且必须对配置了 `is_primary = 1` 的唯一门锁下发凭据。若配置多把主锁,报错阻断 (`CONFIGURATION_ERROR`),严禁随意盲选。 diff --git a/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/security-check.md b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/security-check.md new file mode 100644 index 000000000..32e7bc2c9 --- /dev/null +++ b/docs/reports/dms-r3/real-api-probe/RUN-20260916-01/security-check.md @@ -0,0 +1,30 @@ +# 07 安全指标与敏感数据脱敏核验 (Security Check) + +> **执行批次**:`RUN-20260916-01` +> **敏感信息检查结论**:`ALL_SECURITY_GATES_PASSED` +> **真实密钥泄露数**:`REAL_SECRET_COMMITTED_COUNT = 0` + +--- + +## 1. 核心安全防护指标核查矩阵 + +| 安全校验项 | 强制标准 | 审计方法与验证事实 | 本轮实测值 | 结论 | +| :--- | :---: | :--- | :---: | :---: | +| **明文凭据持久化** | `= 0` | 反射检查所有领域对象 (`TrainDormAuth`, `TrainExternalOperation`, `TrainAccommodation` 等) 是否包含密码/Secret 物理字段 | **0** | `PASS` | +| **日志明文凭证暴露** | `= 0` | 检查 `ReconciliationResult.toString()`、HttpClient 统一日志脱敏拦截器 | **0** | `PASS` | +| **外部操作历史密码暴露** | `= 0` | 检查 `TrainExternalOperation` 实体结构与查询方法,无密码暴露 | **0** | `PASS` | +| **凭据重读/回显端点** | `= 0` | 检查 Controller 中所有 `@GetMapping` 端点,无密码重新读取接口 | **0** | `PASS` | +| **前端界面密钥暴露** | `= 0` | 前端组件与页面均无硬编码 appid/appsecret/token | **0** | `PASS` | +| **真实 Secret Git 提交** | `= 0` | Git diff 扫描与无凭据提交规则核查 | **0** | `PASS` | +| **远程开锁可调用端点** | `= 0` | 全工程搜索 `remote-unlock`,无 Controller 暴露端点 | **0** | `PASS` | +| **007_dicts.sql 执行状态** | 永久禁止 | 数据库脚本零执行 | **0 (未执行)** | `PASS` | + +--- + +## 2. 脱敏规约审查 + +1. **报告与日志脱敏标准**: + - 任何涉及的外部标识符统一执行掩码处理(如:`placeId = ****1234`)。 + - 禁止在任何生成的 Markdown 报告中记录真实的 Cookie Value、Token Value 或数据库连接密码。 +2. **凭据安全交接说明**: + - 系统坚决杜绝在对话或非加密渠道传递生产密钥。后续若需在非生产沙箱执行真实网络连通,仅可通过受本地 `.gitignore` 保护的临时配置文件注入。