状态:已完成(2026-08-22)
实施摘要:
已确认的四点决策:
192.168.1.0/24),IPv4/IPv6 均支持,Python 标准库 ipaddress 校验与匹配;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);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 约束先例)。
ConflictError("该IP已分配给部门,请先取消分配");delete_organization);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 同模式);| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| 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}。
IpEntryMaintenancePanel.tsx(参照 RoleMaintenancePanel 精简为单栏:表格[ipValue/remark/操作] + 分页 + 新建/编辑弹窗 IpEntryEditorModal + 删除 confirm),UI 复用 directoryUi.tsx。OrgIpAssignmentModal:checkbox 列表展示 IP 池(一次拉 pageSize=100,池规模小;超出提示到 IP 列表清理),顶部提示"未分配任何IP时不限制登录地址";保存 PUT 后同步树节点 rowVersion 并刷新树。directoryTypes.ts(IpEntry/IpEntryPage/请求/守卫);新 ipEntryApi.ts(工厂+单例,CRUD 四方法);organizationApi.ts 扩展 getIpAssignments/updateIpAssignments。ip_entry_service.py(normalize/CRUD/分配/登录校验)+ delete_organization 补软删分配行;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);测试基线:后端 171 passed + 3 failed(既有脱节:test_http×2、test_b5_cors×1)+ 385 skipped;前端 358 全绿 + tsc 通过。