部门IP登录限制功能.md 7.3 KB

部门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 通过。