# 部门IP登录限制功能 状态:已完成(2026-08-22) 实施摘要: - 后端:迁移 0015(sys_ip_entry / sys_org_ip_assignment + 审计动作扩展)、ip_entry_service(规范化/CRUD/分配/登录校验)、ip_entries 路由 + 组织分配端点 + 登录接线(ADMIN 豁免)、OpenAPI 1.10.0。 - 后端测试:test_ip_entries.py 18 项全部通过(干净 dms_test 库,真实 MySQL 0015 迁移升级验证通过);既有集成回归的失败项均为测试基础设施既有问题(role_cleanup 无 teardown 残留、conftest attachment_type=OTHER 与 0010 迁移脱节),与本功能无关且不影响程序运行。 - 前端:ipEntryTypes 守卫、ipEntryApi、organizationApi 扩展(getIpAssignments/updateIpAssignments)、IpEntryMaintenancePanel(目录第4个tab「IP地址列表」)、组织树行「分配IP」弹窗(整体替换语义,空选=不限制)。 - 前端验证:tsc --noEmit 通过;vitest 56 文件 368 项全部通过(含新增 ipEntryApi/ipEntryTypes 测试与 directoryApi 的 IP 分配契约测试)。 ## 1. 需求 - 用户组织模块下增加 **IP 地址列表维护**功能(全局单一 IP 池,每条 = 一个 IP 或 CIDR 网段 + 备注); - 可选为每个**部门(组织)分配可登录的 IP 列表**(从池中勾选若干条目); - 用户登录时,若其部门分配了 IP 列表,则校验来源 IP 是否命中列表,未命中拒绝登录;未分配则不限制。 已确认的四点决策: 1. **实体模型**:单一 IP 池(每条一个 IP/网段+备注),部门直接勾选若干条目(不做命名 IP 组); 2. **格式**:支持精确 IP 与 CIDR 网段(如 `192.168.1.0/24`),IPv4/IPv6 均支持,Python 标准库 `ipaddress` 校验与匹配; 3. **校验时机**:仅登录时校验,通过后本会话不再校验; 4. **ADMIN 豁免**:管理员不受部门 IP 限制(防锁死)。 ## 2. 关键设计 ### 2.1 数据模型(迁移 0015_login_ip_restriction) **`sys_ip_entry`**(IP 条目池,BusinessTableMixin): - `ip_value` String(64) NOT NULL:**规范化后**文本——精确 IP 存 `ipaddress.ip_address(x)` 的字符串(IPv6 压缩形式),CIDR 存 `ipaddress.ip_network(x, strict=True)` 的 `网络地址/前缀` 形式; - `remark` String(128) NOT NULL,可空串; - 软删唯一约束 `uq_sys_ip_entry_ip_value_active (ip_value, active_marker)`; - 录入校验:CIDR 主机位必须为 0(`strict=True`,`192.168.1.10/24` 报错提示),避免"以为限一个地址实际放开整个 /24"的语义陷阱。 **`sys_org_ip_assignment`**(部门分配,BusinessTableMixin): - `organization_id` FK sys_organization.id RESTRICT; - `ip_entry_id` FK sys_ip_entry.id RESTRICT; - 软删唯一约束 `(organization_id, ip_entry_id, active_marker)`; - 索引 `ix_sys_org_ip_assignment_org_active (organization_id, is_deleted)` 供登录查询。 部门"分配了IP" = 存在至少一条未删分配行;保存为**整体替换**语义(空数组 = 清空 = 不限制),与方案关联关系一致:内容未变短路、rowVersion 乐观锁、成功后组织 `row_version += 1`。 **审计 CHECK 扩展**(同一迁移):`audit_action_type` 新增 `CREATE_IP_ENTRY / EDIT_IP_ENTRY / DELETE_IP_ENTRY / ASSIGN_ORG_IP`;`audit_target_type` 新增 `IP_ENTRY`(0004/0012 的 drop/recreate 约束先例)。 ### 2.2 引用完整性与删除规则 - **删 IP 条目**:被任何部门有效分配 → `ConflictError("该IP已分配给部门,请先取消分配")`; - **删组织**:既有约束(有子部门/用户拒删)不变;删除组织时**同事务软删其分配行**(补充进 `delete_organization`); - **改 IP 条目的 ip_value**:与其余未删条目重复 → 409。 ### 2.3 登录校验(auth.py,密码与状态校验通过后、签发 token 前) ``` ADMIN 角色 → 放行 无部门 → 放行 部门无有效分配 → 放行 有分配: client_ip 为 None → 拒绝(安全优先) 非法IP文本 → 拒绝 逐条目匹配:CIDR → ip_network 包含(IP版本不同跳过);精确IP → 相等 任一命中 → 放行;否则拒绝 ``` - 复用 `client_ip()`(X-Forwarded-For 优先,与 last_login_ip 同源); - 拒绝错误:`LoginIpForbiddenError`(403,code `LOGIN_IP_FORBIDDEN`,"当前IP不在该部门允许的登录IP范围内"); - 失败审计:`record_login_failure(failure_reason="IP_NOT_ALLOWED")`(自由文本,与 INVALID_CREDENTIALS 同模式); - IP 解析结果不缓存(登录低频,直接查库,无需缓存失效问题)。 ### 2.4 API(版本 1.10.0) | 方法 | 路径 | 权限 | 说明 | |---|---|---|---| | GET | `/api/v1/ip-entries` | ADMIN | 分页 `pageNo/pageSize/keyword`(模糊 ip_value/remark) | | POST | `/api/v1/ip-entries` | ADMIN | `{ipValue, remark}` | | PUT | `/api/v1/ip-entries/{id}` | ADMIN | `{ipValue, remark, rowVersion}` | | DELETE | `/api/v1/ip-entries/{id}?rowVersion=` | ADMIN | 被引用时 409 | | GET | `/api/v1/organizations/{id}/ip-assignments` | ADMIN | `{ipEntryIds: [...]}`(按条目 id 升序) | | PUT | `/api/v1/organizations/{id}/ip-assignments` | ADMIN | `{ipEntryIds, rowVersion}` 整体替换 | 新路由文件 `api/v1/ip_entries.py`;组织分配两条挂在 `organizations.py`。审计动作见 2.1;ASSIGN_ORG_IP 的 target=ORGANIZATION,detail 记录 `{ipEntries: [{id, ipValue}], rowVersion}`。 ### 2.5 前端 - **DirectoryPage 加第 4 个 tab「IP地址管理」**:新组件 `IpEntryMaintenancePanel.tsx`(参照 RoleMaintenancePanel 精简为单栏:表格[ipValue/remark/操作] + 分页 + 新建/编辑弹窗 `IpEntryEditorModal` + 删除 confirm),UI 复用 `directoryUi.tsx`。 - **组织分配 IP**:机构树行内「编辑/删除」旁加「分配IP」按钮 → `OrgIpAssignmentModal`:checkbox 列表展示 IP 池(一次拉 `pageSize=100`,池规模小;超出提示到 IP 列表清理),顶部提示"未分配任何IP时不限制登录地址";保存 PUT 后同步树节点 rowVersion 并刷新树。 - **登录失败**:前端直接展示后端 message(现有机制,无映射表),无需改动。 - 类型与守卫进 `directoryTypes.ts`(IpEntry/IpEntryPage/请求/守卫);新 `ipEntryApi.ts`(工厂+单例,CRUD 四方法);`organizationApi.ts` 扩展 `getIpAssignments/updateIpAssignments`。 ## 3. 实施步骤 1. 模型(IpEntry/OrgIpAssignment)+ 枚举扩展 + 迁移 0015 → 验证:pytest 迁移测试; 2. `ip_entry_service.py`(normalize/CRUD/分配/登录校验)+ `delete_organization` 补软删分配行; 3. 路由(ip_entries.py 新文件 + organizations.py 两条)+ 登录接线 + OpenAPI(1.10.0); 4. 后端测试:新建 `test_ip_entries.py`(单元:normalize 非法参数化/载荷校验;HTTP 集成:CRUD roundtrip、重复409、删被引用409、分配保存/清空、登录 IP 拒绝/放行/ADMIN豁免/无分配放行)+ 同步 test_models(13表)/test_migration(head 0015)/test_openapi_storage 与三处版本断言(1.9.0→1.10.0); 5. 前端类型+API → IpEntryMaintenancePanel + tab → OrgIpAssignmentModal; 6. 前端测试(守卫/API client/scope 断言)+ vitest/tsc/pytest 回归。 测试基线:后端 171 passed + 3 failed(既有脱节:test_http×2、test_b5_cors×1)+ 385 skipped;前端 358 全绿 + tsc 通过。