PEP 752 – 包存储库的隐式命名空间
- 作者:
- Ofek Lev <ofekmeister at gmail.com>, Jarek Potiuk <potiuk at apache.org>
- 发起人:
- Barry Warsaw <barry at python.org>
- PEP 代理人:
- Dustin Ingram <di at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2024年8月13日
- 发布历史:
- 2024年8月18日, 2024年9月7日
摘要
本PEP规定了组织为未来上传保留包名前缀的方法。
“命名空间是一个了不起的创意——让我们多做一些这样的事!” - PEP 20
动机
当前的生态系统缺乏一种让拥有许多包的项目能够表明已验证的所有权模式的方法。这类项目分为两类。
第一类是希望完全控制其命名空间的项目[1]。一些示例:
- 主要云提供商,如亚马逊、谷歌和微软,为每个功能的相应包设置了通用前缀[3]。例如,谷歌的大多数包都以
google-cloud-为前缀,例如用于使用虚拟机的google-cloud-compute。 - OpenTelemetry 是一个用于可观测性的开放标准,其核心API和SDK有官方包,并且有贡献包用于从各种来源收集数据。所有包都以
opentelemetry-为前缀,子前缀形式为opentelemetry-<component>-<name>-。贡献包位于中央存储库中,并且只有它们具有发布能力。 - Apache Airflow 是一个用于以编程方式编排、调度和监控工作流的平台。它有提供者(providers),每个提供者包都以
apache-airflow-providers-为前缀。
第二类项目[2]希望共享其命名空间,以便一些包由官方维护,并鼓励第三方开发者通过发布自己的包来参与。一些示例:
- Project Jupyter 致力于开发用于共享交互式文档的工具。他们支持扩展,这些扩展在大多数情况下(以及所有官方维护的扩展)都以
jupyter-为前缀。 - Django 是现存最广泛使用的Web框架之一。他们有可复用应用的概念,这些应用通常通过第三方包安装,这些包实现了部分功能来扩展基于Django的网站。这些包通常以
django-或dj-为前缀。
这类项目特别容易受到名称抢占攻击,这最终可能导致依赖混淆。
例如,假设发布了一个新产品,对其进行监控很有价值。合理地假设 Datadog 最终会支持它作为官方集成。由于路线图优先级和实现所需的时间,提供这样的集成需要相当长的时间。不可能保留所有潜在包的名称,因此在此期间,攻击者可能会创建一个看似合法的包,并在运行时执行恶意代码。用户不仅更可能安装此类包,而且这样做会损害整个项目的形象。
尽管PEP 708试图解决这个攻击向量,但它专门针对依赖解析期间考虑多个存储库的情况,并且不为上述用例提供任何保护。
命名空间还将大大减少拼写错误抢注的发生率,因为拼写错误必须在前缀本身中,而前缀是规范化的,并且很可能是一个简短的、众所周知的标识符,如 aws-。近年来,拼写错误抢注已成为流行的攻击向量[4]。
PyPI使用的当前针对拼写错误抢注的保护是规范化相似字符,但这对于这些用例来说是不够的。
命名空间将解决的另一个问题是为遵循约定命名模式的包选择新名称。通常(例如 Apache Airflow 就是这种情况),在决定创建新包之前会有公开讨论。该决定基于商定的名称并遵循现有包的模式。如果在讨论中考虑了更多的包名称,所有名称都必须在讨论公开之前通过 PyPI 界面保留,否则名称可能会被其他用户占用。这在过去已经发生过,如相关讨论中所述。
基本原理
其他包生态系统通常通过以下两种方法之一解决了这个问题:最小化或最大化向后兼容性。
- NPM 具有作用域包的概念,这主要是为了解决可用良好包名称匮乏的问题(无论是真实的还是感知到的现象)而引入的。当用户或组织注册时,会给他们一个与其名称匹配的作用域。例如,用于使用 Google Cloud Storage 的包是
@google-cloud/storage,其中@google-cloud/是作用域。普通用户帐户(非组织)可以发布用于公共使用的无作用域包。这种方法的向后兼容性最低,因为每个安装程序和工具都必须修改以适应作用域。 - NuGet 具有包ID前缀保留的概念,这主要是为了满足希望知道包来源的用户而引入的。包名前缀可以由一个或多个所有者保留。每个保留的包在其页面上都有一个特殊指示来传达这一点。保留后,如果用户不是前缀的所有者,则带有保留前缀的任何上传都将失败。拥有前缀的现有包可以照常发布。这种方法的向后兼容性最高,因为只需要修改 PyPI 等索引,安装程序无需更改。
本PEP指定了NuGet在扁平命名空间中进行授权保留的方法。任何需要新包语法的解决方案都必须建立在现有扁平命名空间之上,因此通过保留机制获得的隐式命名空间将是此类显式命名空间的先决条件。
尽管匹配保留命名空间的现有包将不受影响,但阻止未来的未经授权的上传,并战略性地应用PEP 541的恶意情况删除请求,将把用户的风险降低到可忽略的水平。
术语
本文件中的关键词“必须”、“不得”、“需要”、“应”、“不应”、“建议”、“不建议”、“可以”和“可选”应根据RFC 2119的描述进行解释。
- 组织
- 组织是拥有项目并拥有与其关联的各种用户的实体。
- 授权
- 授权是为包存储库保留命名空间。
- 开放命名空间
- 开放命名空间允许任何项目所有者上传。
- 受限命名空间
- 受限命名空间只允许命名空间的所有者上传。
- 父命名空间
- 命名空间的父级是指不带尾随连字符组件的命名空间,例如
foo-bar的父级是foo。 - 子命名空间
- 命名空间的子级是指带有额外尾随连字符组件的命名空间,例如
foo-bar是foo的有效子级,foo-bar-baz也是。
规范
组织
任何允许创建项目的包存储库(例如非镜像)可以提供组织概念[6]。组织是拥有项目并拥有与其关联的各种用户的实体。
组织可以保留一个或多个命名空间。此类保留既不授予现有项目所有权,也不授予其特殊权限。
命名
语义
命名空间授权赋予以下所有权:
- 与命名空间本身匹配的项目,例如占位符包 microsoft。
- 以命名空间后跟连字符开头的项目。例如,命名空间
foo将匹配规范化的项目名称foo-bar,但不匹配项目名称foobar。
包名称匹配作用于规范化命名空间。
命名空间是针对每个包存储库的,不得在存储库之间共享。例如,如果 PyPI 有一个由微软公司拥有的命名空间 microsoft,那么来自其他非 PyPI 镜像存储库的以 microsoft- 开头的包不享有同等程度的信任。
授权不得重叠。例如,如果已存在 foo-bar 的授权,则禁止为 foo 授予新授权。重叠的判断方法是,将提议的规范化命名空间与每个现有根授权的规范化命名空间进行比较。每次比较都必须在提议的和现有命名空间的末尾添加一个连字符。当任何现有命名空间以提议的命名空间开头时,即检测到重叠。
上传
如果要上传的包的名称与保留的命名空间匹配,并且以下任一条件为真:
- 该项目尚不存在。
- 该项目不归拥有该命名空间的有效授权的组织所有。
则上传必须以 403 HTTP 状态码失败。
开放命名空间
授权所有者可以选择允许其他人发布带有相关命名空间的新项目。这样做必须允许任何用户为匹配该命名空间的新项目进行上传。
命名空间的所有者可以将其设为开放,并允许其他组织使用该授权。在这种情况下,被授权的组织没有特殊权限,与没有所有权的开放授权等效。
存储库元数据
JSON API 版本将从 1.2 增加到 1.3。支持此PEP的存储库必须实现以下API更改。不支持此PEP的存储库不得实现这些更改,以便API消费者能够确定存储库是否支持此PEP。
项目详情
项目详情响应将按如下方式修改。
如果项目不匹配任何有效的命名空间授权,namespace 键必须为 null。如果项目匹配命名空间授权,则该值必须是一个包含以下键的映射:
命名空间详情
此URL的格式为 /namespace/<namespace>,其中 <namespace> 是规范化命名空间。例如,命名空间 foo.bar 的URL将是 /namespace/foo-bar。
响应将是一个包含以下键的映射:
授权移除
当保留的命名空间变为未声明时,存储库必须在API中将 namespace 键设置为 null。
以前已被声明但现在未被声明的命名空间应有资格被任何组织再次声明。
社区支持
以下组织的代表已表示支持此PEP(并附上讨论链接)
- Apache Airflow (扩展)
- pytest
- Typeshed
- Project Jupyter (扩展)
- Microsoft
- Sentry(倾向于NuGet方法而非其他方法,但不受当前能力不足的负面影响)
- DataDog
向后兼容性
没有内在的担忧,因为仍然是一个扁平的命名空间,安装程序无需修改。此外,许多项目已经选择用前缀来表示共同目的,就像typeshed 所做的那样。
安全隐患
如何教授此内容
对于包消费者,我们将记录元数据如何在API中公开,并可能在未来注意到支持利用命名空间在安装期间提供额外安全保证的工具。
参考实现
此PEP的完整参考实现可在PR #17691中找到。
被拒绝的想法
授予用户保留权
由于包存储库是扁平命名空间,允许任何用户保留命名空间是不可行的,不仅因为存在有限资源的争夺,还因为没有存储库有足够的人力操作员来管理任意数量用户的审查。
工件级命名空间关联
本PEP的早期版本曾提议在发布时将元数据与单个工件关联。此提案被拒绝,因为它可能导致用户困惑,用户可能会根据当前的授权而非发布时间,期望命名空间授权保证在项目级别。
组织范围划分
本PEP的主要动机是减少依赖混淆攻击,而NPM风格的作用域与允许遗留扁平命名空间会增加风险。如果文档指示用户在命名空间 foo 中安装 bar,则用户必须小心安装 @foo/bar 而不是 foo-bar,反之亦然。Python包生态系统有名称规范化规则,以最大限度地提高通信便利性,而这将是一种倒退。
Python的运行时环境也不利于作用域。虽然同一JavaScript包的多个版本可以共存,但Python只允许一个全局命名空间。除非对语言本身进行重大更改,否则这几乎不可能改变。此外,用户已经习惯了包名通常与他们导入的名称相同,取消扁平命名空间将废除这一惯例。
作用域将特别受到随着时间推移必然发生的组织变更的影响。一个组织可能会因为内部重组、收购或任何其他原因而更改其名称。每当发生这种情况时,他们拥有的每个项目实际上都会被重命名,这会给用户带来不必要的混乱,而且经常发生。
最后,对社区的破坏将是巨大的,因为它需要每个包管理器、安全扫描器、IDE等进行更新。以作用域发布的新包将与旧工具不兼容,并会给用户带来困惑,同时维护者也不得不处理这些投诉而感到沮丧。
鼓励专用包存储库
至关重要的是,这给项目带来了维护自身基础设施的负担。对于绝大多数公司来说,这是一个不切实际的期望,对于社区项目来说更是完全无法接受。
这在大多数情况下没有帮助,因为大多数包管理器的默认行为是使用 PyPI,所以尝试执行简单 pip install 的用户已经容易受到恶意包的攻击。
在这个理论化的未来中,每个项目都必须记录如何将其存储库添加到依赖解析中,这对于每个包管理器都会有所不同。很少有包管理器能够从特定存储库下载特定依赖项,并且在常见情况下会要求用户使用冗长的配置。
不支持此功能的将改为通过存储库的有序枚举来查找给定包,从而导致依赖混淆。例如,假设用户需要来自两个自定义存储库 X 和 Y 的两个包。如果每个存储库都包含这两个包,但其中一个在 X 上是恶意的,另一个在 Y 上是恶意的,那么用户将无法在不遇到恶意包的情况下满足其需求。
独家依赖来源断言
这里的想法[5]是设计一种通用方法,让客户端能够进行来源断言,以验证依赖项的某些属性,每个属性都有自定义语法。一些示例:
- 该包由特定的组织或用户名上传,例如
pip install "azure-loganalytics from microsoft" - 该包由特定域名所有者上传,例如
pip install "google-cloud-compute from cloud.google.com" - 该包由具有特定电子邮件地址的用户上传,例如
pip install "aws-cdk-lib from contact@amazon.com" - 匹配命名空间的包由授权方上传(本PEP)
一个根本的缺点是它与多个存储库的协作不佳。例如,假设用户需要 azure-loganalytics 包,并希望确保它来自名为 microsoft 的组织。如果微软在 PyPI 上的组织名称是 microsoft,那么默认使用 PyPI 的包管理器可以接受 azure-loganalytics from microsoft。但是,如果依赖解析使用了多个存储库,那么用户就必须将存储库指定为定义的一部分,这对于断言包所有者名称的专门章节中所述的原因来说是不切实际的。
这种方法的另一个普遍弱点是,用户尝试执行不带特殊语法的简单 pip install(这是最常见的场景)时,已经容易受到恶意包的攻击。为了克服这一点,必须有某种默认的信任机制,在所有情况下,这都会给每个工具强加特定的用户体验或解析器逻辑。
例如,可以更改包管理器,以便在第一次安装包时,用户会收到一个确认提示,显示来源详细信息。这会非常令人困惑和嘈杂,特别是对于新用户,并且对于现有用户来说,这将是一个破坏性的用户体验更改。许多安装方法在此场景下都无法工作,例如在CI中运行或从requirements文件中安装,用户可能会收到数百个提示。
一种使这种方式对用户干扰较小的解决方案是手动维护一份可信赖详细信息(组织/用户名称、域名、电子邮件地址等)的列表。这可以通过提供入口点的包来发现,包管理器可以学习检测这些入口点,并且企业环境可以默认安装。这有一个主要缺点,即无法提供自动保证,这将限制其对更容易受影响的普通用户的有用性。
有两种想法可以提供自动保护,它们可以基于PEP 740证明或利用托管元数据的第三方API的新机制。
首先,每个存储库都可以提供一项服务,根据他们认为适当的任何标准来验证包的所有者。验证后,存储库会将详细信息添加到默认安装的专用包中。
这将需要专门的维护,这对于大多数存储库(即使是当前的 PyPI)来说都是不现实的。目前尚不清楚如何支持没有域名等资源的社区项目。至关重要的是,在存在多个存储库的情况下,此解决方案将给用户带来额外的困惑,因为每个存储库都可能有自己的验证过程、证明标准和包含已验证详细信息的默认包。让每个包管理器都了解每个存储库选择的验证包并在依赖解析之前默认安装它,这将是一项挑战,难以获得社区的认可。
如果选择数字证明作为机制,一个缺点是,在自定义包存储库中实现这将需要大量工作。对于 PyPI 而言,可信发布的先决工作以及PEP 740 自身的实现花费了一名全职工程师一年时间,其时间由企业赞助商支付。其他组织不太可能实施类似的工作,因为更简单的机制使得实现可重现构建成为可能。当一切都在内部管理时,证明的用处也不大。社区项目不太可能承担这项工作,因为它们可能缺乏维护必要基础设施的资源,而且鼓励专用包存储库存在重大缺点。
另一个想法是将来源断言托管在外部,并将更多逻辑推送到客户端。一种可能的实现是指定一个来源API,该API可以托管在指定的相对路径(例如 /provenance)下。然后,每个存储库上的项目可以配置为指向特定域名,此信息将在安装期间传递给客户端。
虽然这种分布式方法对存储库的基础设施负担较小,但它有可能带来安全风险。如果外部来源API受到威胁,可能会导致恶意软件包被安装。如果外部API宕机,则可能导致软件包安装失败,或者软件包管理器可能只会发出警告,在这种情况下,就没有安全优势。
此外,这对没有资源维护此类 API 的社区项目不利。他们可以使用免费托管解决方案,就像许多文档所做的那样,但他们没有从技术上拥有基础设施,如果慷慨的服务受到限制,他们就会受到损害。
最后,尽管这两种理论方法都尚未具有规定性,但它们暗示了工件级别的断言,而这已经是一个被拒绝的想法。
断言包所有者名称
这是关于断言包来自特定的组织或用户名称。它与组织作用域划分的想法非常相似,只是以扁平命名空间为基本假设。
这将需要修改每个受支持存储库的JSON API,可以通过公开额外元数据或作为适当的来源断言来实现。
与组织作用域划分的想法一样,需要一种新的语法,例如 microsoft::azure-loganalytics,其中 microsoft 是组织,azure-loganalytics 是包。尽管与现有的扁平命名空间配合良好,但它保留了对社区造成破坏的关键缺点,因为需要进行大量更改。
一个独特的缺点是名称是存储库的实现细节。在PyPI上,组织的名称与用户名称是分开的,因此存在冲突的可能性。在多个存储库的情况下,用户可能会遇到类似于鼓励专用包存储库被拒绝的想法末尾所描述的依赖混淆情况。
为了缓解这个问题,有人建议将语法扩展,以包含预期的存储库URL,例如 microsoft@pypi.org::azure-loganalytics。这种语法或类似的东西过于冗长,可能导致用户困惑,更糟糕的是,如果它在那些能够维护专用基础设施的人群中获得更多采用,将导致沮丧(社区项目将无法受益)。
扩展语法试图在依赖说明符中标准化解析器行为和配置。这不仅会强制规定工具的用户体验,而且在具有或不具有包存储库概念的语言生态系统的包管理器中也缺乏先例。在这种情况下,解析器配置与依赖定义是分开的。
| 语言 | 工具 | 解析行为 |
|---|---|---|
| Rust | Cargo | 依赖解析可以在 Cargo.toml 中使用 [patch] 表修改。 |
| JS | Yarn | 尽管它们有协议的概念(类似于我们直接引用的URL方案),但用户在 package.json 文件中配置解析字段。 |
| JS | npm | 用户可以在 package.json 文件中配置 overrides 字段。 |
| Ruby | Bundler | Gemfile 允许为 gem 指定显式来源。 |
| C# | NuGet | 可以通过配置 Directory.Packages.props 文件来覆盖包版本。 |
| PHP | Composer | composer.json 文件允许为特定包指定存储库源。 |
| Go | go | go.mod 文件允许指定 replace 指令。请注意,这既用于直接依赖,也用于传递依赖。 |
使用固定前缀
这里的想法是有一个或多个顶级固定前缀用于命名空间保留。
com-: 为企业组织保留。org-: 为社区组织保留。
组织随后将申请以其组织类型为前缀的命名空间。
这将造成永久性的混乱,因为项目启动时,不知道用户群是否足够大以保证命名空间保留。每当发生这种情况时,项目都必须重命名,这将给项目维护者带来高昂的维护负担,并给不得不学习新的引用项目包方式的用户造成困惑。这很可能会阻止项目保留命名空间。
这种方法的另一个问题是项目通常考虑到品牌形象(示例),并且不愿更改其包名称。
期望每个公司和项目都自愿更改其现有和未来的包名称是不现实的。
使用DNS
这里的想法是在API中为项目添加一个名为 domain-authority 的新元数据字段。存储库将支持一个用于通过HTTPS验证域的新端点。然后客户端将支持允许某些域的选项。
这并未解决目标受众的问题,因为他们不检查其包的来源,而更多是关于检查上传的完整性,这已通过PEP 740以更安全的方式得到支持。
大多数项目没有域名,无法从中受益,这不公平地偏袒了有财力获取域名的组织。
未解决的问题
目前没有。
脚注
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0752.rst