OpenMed 结构化访问复核(Structured Access Review):本地、确定性、无值的字段权限核对机制
2026/9/19 23:07:54 网站建设 项目流程

OpenMed 结构化访问复核(Structured Access Review):本地、确定性、无值的字段权限核对机制

【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed

本文基于 docs/security/access-review.md 编写。OpenMed 是一个本地优先(local-first)的医疗 AI 与 HIPAA PII 去标识化项目,其风险模块提供了一套结构化访问复核机制:将工作流(workflow)声明的read/export字段集合与资源 Schema、可选的拒绝(deny)策略进行本地比对,输出三类复核发现(Missing / Excessive / Denied),并渲染为确定性的 JSON 与 Markdown 报告。读完本文,你将掌握openmed.risk.review_structured_access的完整调用方式、报告结构、输入约束与安全边界,并了解如何在集成配置阶段用它做最小必要(minimum-necessary)字段的配置证据核验。

一、功能定位:配置证据,而非合规认证

openmed.risk.review_structured_access回答的是一个范围极窄的治理问题:

工作流为readexport两种访问模式声明的字段,是否恰好落在资源 Schema 暴露的字段集合、以及调用方拒绝策略所允许的范围之内?

该函数是集成配置的本地复核辅助工具(local review aid),它:

  • 不是合规认证(compliance certification);
  • 不是临床决策保证(clinical decision guarantee);
  • 不授予任何访问权限
  • 不证明某个集成真的强制执行了这份声明;
  • 不检查任何记录(record)内容
  • 不发起任何网络调用

复核结果应当被当作**配置证据(configuration evidence)**使用。一份完整(complete)的报告只意味着:对于所有已声明的工作流与访问模式,不存在 Missing、Excessive、Denied 三类发现中的任何一项。

二、快速上手:最小可用示例

以下示例来自文档,完整可直接运行:

from openmed.risk import render_access_review, review_structured_access report = review_structured_access( { "triage": { "read": {"patient_id", "age"}, "export": {"diagnosis"}, } }, { "properties": { "patient_id": {}, "age": {}, "diagnosis": {}, "notes": {}, } }, denied_fields={"export": {"diagnosis"}}, ) print(report.to_json()) print(render_access_review(report))

其中:

  • 第一个参数workflow_requirements:工作流名 → 字段声明的映射;
  • 第二个参数resource_schema:资源 Schema(支持多种形状,见下文);
  • 关键字参数denied_fields:全局或按模式(mode)区分的拒绝字段集合。

三、报告内容:三类复核发现

针对每个访问模式(read/export),报告识别三类发现:

发现类别含义
Missing(缺失)工作流声明了该字段,但资源 Schema 中不存在该字段——可能是拼写错误或依赖了已删除的列
Excessive(越界)Schema 中存在该字段,但当前工作流与访问模式没有声明它——意味着潜在的超范围访问
Denied(拒绝)声明的字段命中全局或模式级拒绝策略——配置与策略冲突,需要显式处理

以文档示例为例:资源 Schema 拥有patient_idagediagnosisnotes四个字段;triage工作流声明read = {patient_id, age}export = {diagnosis};拒绝策略声明export模式拒绝diagnosis。复核结果:

  • read模式:requested = {patient_id, age}available = {patient_id, age, diagnosis, notes},无 Missing、无 Denied,但Excessive = {diagnosis, notes}
  • export模式:requested = {diagnosis}available同上,Denied = {diagnosis}Excessive = {patient_id, age, notes}
  • 整体complete == False(存在 Excessive 与 Denied 发现)。

这里有一个反直觉但重要的设计:Excessive 是常态而非异常。因为 Excessive 的定义是「Schema 中有、但本工作流本模式未声明」的字段,只要资源 Schema 比工作流声明的字段宽,就一定会出现。所以一个**完整(complete)**的结果,要求工作流声明恰好覆盖 Schema 的全部字段——这在语义上等价于「声明的字段集合必须与 Schema 完全一致」;配合 deny 策略时,还需要被拒绝的字段不在声明集合内。这一点在 源码实现 中可以看到:

denied = global_denied_set | set(mode_denied.get(mode, ())) denied_requested = requested_set & denied mode_reviews[mode] = AccessModeReview( mode=mode, requested_fields=requested, available_fields=resource_fields, allowed_fields=tuple(sorted((requested_set & resource_set) - denied)), missing_fields=tuple(sorted(requested_set - resource_set)), excessive_fields=tuple(sorted(resource_set - requested_set)), denied_fields=tuple(sorted(denied_requested)), )
  • missing = requested - available(声明了但 Schema 没有);
  • excessive = available - requested(Schema 有但未声明);
  • denied = requested ∩ denied(声明了但被策略拒绝);
  • allowed = (requested ∩ available) - denied

四个集合互斥,恰好覆盖全部字段关系。测试用例 tests/unit/risk/test_access_review.py#L47-L60 完整验证了这三类发现的判定逻辑。

四、Schema 映射值被忽略:报告永不携带记录值

一个至关重要的隐私设计是:Schema 的映射值(mapping values)一律被忽略。Schema 中的exampledefaultdescription等元数据不会进入报告,报告只包含:

  • 经过校验的结构性字段名与工作流名;
  • 访问决策(decision);
  • 各类计数(counts)。

在 openmed/risk/access_review.py#L103-L125 的_field_tuple中可以看到:当字段声明是Mapping时,只取value.keys(),绝不遍历值:

if isinstance(value, Mapping): # A mapping is a convenient schema/field declaration. Its values may # contain examples or defaults and are intentionally never traversed. candidates = value.keys()

因此,即使 Schema 中写着{"patient_id": {"example": "PATIENT-0007"}},报告里也永远不会出现"PATIENT-0007"。测试 tests/unit/risk/test_access_review.py#L63-L87 明确断言:PATIENT-0007PRIVATE-DIAGNOSISRAW-SCHEMA-VALUE等值绝不会出现在序列化后的 JSON 中,而结构性的patient_id会保留。

这也解释了「非法名称被拒绝时不会在异常中回显」的设计:调用方完全可能把敏感值误填在字段名位置,若异常消息回显输入值,就会击穿报告的价值边界。因此所有校验错误消息只描述「输入位置与类型」,从不回显具体值——参见_identifier的注释与实现 openmed/risk/access_review.py#L74-L84,以及测试 tests/unit/risk/test_access_review.py#L123-L132(SENSITIVE-RAW-VALUE不出现在异常字符串中)。

五、输入形状:灵活而受控

5.1 资源 Schema 的四种形状

resource_schema参数(类型ResourceSchema)支持多种常见形状,由 _resource_fields 统一提取字段名:

  1. JSON Schema 风格映射:含properties键的映射(如{"properties": {"patient_id": {...}, ...}});
  2. 普通映射:以字段名为键(此时fields键也可用);
  3. 带属性的对象:暴露fieldscolumnsnames属性的对象;
  4. 可迭代的字段名集合:如["patient_id", "age", ...]或集合。

无论哪种形状,映射的值一律不检查,只提取键名。

5.2 工作流声明的多种写法

workflow_requirements支持:

  • 映射形式{"triage": {"read": {...}, "export": {...}}}
  • 简写形式:如果键只有read/export(或其长写法read_fields/export_fields),会被自动包装为名为default的工作流;
  • 单个带名称的声明:含nameworkflow键的映射(两者不能同时出现);
  • WorkflowRequirement对象序列WorkflowRequirement(name, read_fields=[...], export_fields=[...]),见 openmed/risk/access_review.py#L173-L214 与测试 tests/unit/risk/test_access_review.py#L104-L120。

访问模式键支持短写法与长写法两种拼写:read/exportread_fields/export_fields。但同一模式不能同时声明两次(例如同时写readread_fields会报错),模式对象中也不能出现未知键。

5.3 拒绝策略的全局与模式级写法

denied_fields支持:

  • 普通字段集合{"diagnosis"}["diagnosis"]—— 全局拒绝;
  • 策略映射{"all": [...], "read": [...], "export": [...]}——all表示全局拒绝,read/export表示模式级拒绝。策略映射中只允许这三个键,出现其他键会被拒绝(见 _normalize_denied_fields)。

六、安全边界与资源上限

复核机制对输入施加了严格的边界约束(常量定义见 openmed/risk/access_review.py#L24-L36):

约束限制
字段标识符最多 128 个 ASCII 结构字符,且须匹配[A-Za-z_][A-Za-z0-9_.:-]{0,127}_SAFE_IDENTIFIER
字段声明总数至多 4,096(_MAX_FIELDS
工作流总数至多 128(_MAX_WORKFLOWS
非法名称处理拒绝,且异常消息不含输入值
超大/异常迭代器以通用、无值(value-free)的错误拒绝

这些边界由_bounded_tuple在物化迭代器时强制实施 openmed/risk/access_review.py#L87-L100:超过上限即抛AccessReviewValidationError,迭代过程中抛出的任意异常(RuntimeErrorTypeErrorMemoryError等)都会被包装成不带原始值、不带__cause__链的通用错误("structured access review declarations are invalid")——测试 tests/unit/risk/test_access_review.py#L146-L178 验证了这一行为。

其余被拒绝的输入还包括:

  • 歧义的访问模式别名(readread_fields并存);
  • 模式对象或 deny 策略对象中的未知键;
  • 同一工作流重复命名、nameworkflow并存;
  • AccessReviewReport/AccessModeReview等公开类型被构造为自相矛盾的发现(例如allowedrequested/available/denied不一致),见 openmed/risk/access_review.py#L229-L261。

这些边界保证:输入无论多畸形,都不可能把原始内容注入报告或异常消息,报告的隐私边界因此成立。

七、确定性输出:等价的声明产生逐字节相同的报告

实现完全基于集合运算与排序:

  • 字段名与工作流名在构造时被规范化为排序去重的元组
  • 工作流按名称排序,模式按固定的readexport顺序输出;
  • JSON 序列化使用sort_keys=Trueensure_ascii=Trueallow_nan=False(见 to_json)。

因此,等价声明产生的 JSON 与 Markdown 逐字节相同。测试 tests/unit/risk/test_access_review.py#L63-L87 用两种不同书写顺序的等价声明验证了first.to_json() == second.to_json()。这对在 CI 中做配置漂移检测(diff)非常友好:配置未变,报告哈希不变。

7.1 报告对象 API

AccessReviewReport是返回值,也是公开的不可变数据类 openmed/risk/access_review.py#L347-L540,主要成员:

  • report.workflow(name):按名称取单个工作流复核;
  • report.workflows_with_findings:含有任何发现的工作流;
  • report.missing_fields/report.excessive_fields/report.denied_fields:跨工作流的去重字段;
  • report.summary:聚合计数(workflow_countworkflows_with_findingsresource_field_countmissing_field_countexcessive_field_countdenied_field_countcomplete);
  • report.complete:所有工作流所有模式均无发现时为True
  • report.to_dict()/report.to_json(indent=2)/report.to_markdown():三种确定性输出;
  • report.schema_versionACCESS_REVIEW_SCHEMA_VERSION = 1

render_access_review(report)是对report.to_markdown()的包装,要求传入真正的AccessReviewReport实例 openmed/risk/access_review.py#L790-L795。Markdown 输出包含摘要表(Workflows / Resource fields / Missing / Excessive / Denied / Complete)与每个工作流的发现表(Access / Missing fields / Excessive fields / Denied fields / Status,状态为passreview)。

此外,源码为保持 API 可发现性提供了三个等价别名:build_access_review_reportaccess_review_reportreview_access,均指向同一实现 openmed/risk/access_review.py#L798-L802,并从 openmed/risk/init.py 对外导出。

八、命名与报告即元数据:字段名必须无 PHI

由于报告会原样呈现字段名与工作流名,字段名和工作流名属于报告可见的元数据,绝不能包含受保护健康信息(PHI)。例如不要用{"workflow": "patient-john-doe", "read": {"disease-name-..."}}这类命名。每个字段标识符限定为 128 个 ASCII 结构字符(正则[A-Za-z_][A-Za-z0-9_.:-]{0,127}),这意味着字段名中不允许出现空格、非 ASCII 字符与特殊字符,天然阻断了将自由文本(如"fever 39.2C, chest pain")误作字段名注入报告的可能。

九、本地性与运行前提

复核实现不执行任何强制的网络调用,也不检查任何记录。整条计算链在 review_structured_access 中完成:提取 Schema 字段 → 规范化拒绝策略 → 规范化工作流 → 逐模式集合比对 → 组装报告。没有模型推理、没有远端服务、没有记录扫描,因此可以在离线环境、CI 流水线或集成配置评审中随时运行,且执行成本可忽略。

适用前提:本机制针对的是结构化(structured)字段访问声明的核对,并不适用于非结构化自由文本的去标识化校验;后者应配合 OpenMed 核心 PII 去标识化能力使用(参见 docs/anonymization.md 与 docs/security/minimum-necessary.md)。

十、与周边模块的协同:配置证据链

结构化访问复核并非孤岛,它与 OpenMed 风险/合规模块中的同类确定性机制构成一套「配置证据链」:

  • docs/compliance/access-review-expiry.mdopenmed.compliance.access_review_expiry为结构化访问复核提供过期门禁(expiry gate)——校验复核的签发时间、排他性过期边界、策略指纹(policy fingerprint)与必需决策类别,输出稳定的pass/block与错误码(not_yet_validexpiredpolicy_fingerprint_mismatchmissing_decision_categories)。两者共享相同的设计哲学:仅含白名单元数据、忽略映射值、错误不回显输入、完全离线确定。若要将访问复核接入发布自动化,建议配合该过期门禁一起使用;
  • openmed/risk/minimum_necessary.py:最小必要字段选择器——按「用途(purpose)声明 + 策略 profile」从源记录中挑选导出字段,与访问复核一样保持「策略声明与记录值分离」「全部调用方元数据有界」的设计。访问复核回答「声明是否与 Schema/策略一致」,最小必要选择器回答「按声明实际导出哪些字段」,二者可组成「声明核对 → 实际导出」的完整最小必要链路;
  • docs/security/access-scope.md等安全文档提供了访问范围相关的策略背景。

十一、结论与使用建议

openmed.risk.review_structured_access是 OpenMed 中一类少见的「纯治理」工具:它不读取一行记录,不发起一次网络请求,仅凭三份输入(工作流声明、资源 Schema、拒绝策略)就能产出确定性的、无值的结构化访问复核报告。使用时请牢记其边界:

  1. 把它当作配置证据:报告完整 ≠ 实际访问已获授权,也不证明集成真的强制执行声明;
  2. 命名必须无 PHI:字段名与工作流名会原样进入报告与异常消息;
  3. 配合过期门禁与最小必要选择器使用,形成可审计、可复现的发布前核验链;
  4. 利用确定性输出做漂移检测:将to_json()结果纳入 CI 基线,配置变更即产生可 diff 的报告差异。

在集成开发或对接外部系统时,将本文示例中的triage替换为你的真实工作流名、properties替换为对接资源的真实字段清单、denied_fields替换为你的策略声明,即可在代码评审与发布门禁中获得一份可引用、可复现的结构化访问证据。

【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询