PEP 708 – 扩展仓库 API 以减轻依赖混淆攻击
- 作者:
- Donald Stufft <donald at stufft.io>
- PEP 委托:
- Paul Moore <p.f.moore at gmail.com>
- 讨论列表:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建:
- 2023年2月20日
- 发布历史:
- 2023年2月1日, 2023年2月23日
- 决议:
- Discourse 消息
草案通过
此 PEP 已**草案通过**,在 PEP 正式发布之前,需要满足以下条件:
- 在 PyPI(仓库)中实现 PEP,包括允许项目所有者设置跟踪数据的任何必要的 UI 元素。
- 在 PyPI 之外的至少一个仓库中实现 PEP,因为如果没有至少两个索引,就无法真正测试合并索引。
- 在 pip 中实现 PEP,支持预期的语义,并可用于演示预期达成的安全益处。此实现最初需要“默认关闭”,这意味着用户必须选择加入测试它。理想情况下,我们应该收集来自已成功试用新功能的用户(项目所有者和项目用户)的明确正面报告,而不仅仅是假设“没有消息就是好消息”。
摘要
依赖混淆攻击,即安装恶意软件包而不是用户期望的软件包,是一种日益普遍的供应链威胁。针对 Python 依赖项的大多数此类攻击,包括最近的 PyTorch 事件,都发生在多个软件包仓库中,其中期望来自一个仓库(例如自定义索引)的依赖项从另一个仓库(例如 PyPI)安装。
为了帮助解决此问题,此 PEP 提出扩展简单仓库 API,以允许仓库运营者指示在他们的仓库中找到的项目“跟踪”不同仓库中的项目,并允许项目跨多个仓库扩展其命名空间。
这些功能将允许安装程序确定何时从特定仓库组合提供项目是预期的并且应该被允许,以及何时不是预期的并且应该停止安装并显示错误以保护用户。
动机
有一种长期存在的攻击类型称为“依赖混淆”攻击,大致归结为单个用户期望获得软件包A
,但他们却得到了B
。在 Python 中,这几乎总是由于多个仓库的配置(可能包括 PyPI 的默认配置)导致的,用户期望软件包A
来自仓库X
,但有人能够在同一名称下将软件包B
发布到仓库Y
。
依赖混淆攻击长期以来一直可能存在,但最近随着这些攻击成功执行的公开案例而受到关注。
一个具体的例子是最近 PyTorch 项目有一个名为torchtriton
的内部软件包,该软件包只打算从其位于https://download.pytorch.org/
的仓库中安装,但该仓库设计为与 PyPI 结合使用,并且torchtriton
的名称未在 PyPI 上被占用,这使得攻击者可以使用该名称并发布恶意版本。
今天有很多方法可以减轻这些攻击,但它们都要求最终用户主动采取措施来保护自己,而不是默认受到保护。这意味着对于绝大多数用户来说,即使他们最终了解了这些类型的攻击,他们也可能仍然容易受到攻击。
最终,这些攻击的根本原因在于,所有 Python 软件包名称都没有一个全局唯一的命名空间。相反,每个仓库都是它自己独立的命名空间,并且当给出一个“抽象”名称(例如spam
)进行安装时,安装程序必须将其隐式转换为“具体”名称,例如pypi.org:spam
或example.com:spam
。目前,Python 安装工具中的标准行为是将这些多个命名空间隐式地展平为一个包含所有命名空间文件的命名空间。
这种将命名空间折叠起来的假设被认为是预期的,这意味着当不同仓库中具有相同名称的软件包由不同的方(如torchtriton
案例)创作时,依赖混淆攻击就成为可能。
这使得问题变得非常棘手,因为没有“正确”的答案;既有希望将两个仓库合并到一个命名空间中的有效用例,也有希望将两个仓库视为独立命名空间的有效用例。这意味着安装程序需要某种机制来确定何时应该合并多个仓库的命名空间以及何时不应该合并,而不是一成不变的始终合并或从不合并规则。
此功能可以推送到最终用户,因为最终用户才是其对从哪个仓库安装什么内容的期望真正起作用的人。但是,通过扩展仓库规范以允许仓库指示何时安全,我们可以在项目自然跨越多个独立命名空间时,使各个项目和仓库能够“默认工作”,同时保持安装程序能够默认安全的能力。
就其本身而言,此 PEP 并不能解决依赖混淆攻击,但它所做的是提供足够的信息,以便安装程序可以防止它们,而不会对其他有效和安全的用例造成过多的附带损害。
基本原理
此 PEP 旨在支持跨仓库合并名称的两个主要用例。
第一个用例是当一个仓库没有定义自己的名称,而是扩展其他仓库中定义的名称时。这通常发生在将项目从一个仓库镜像到另一个仓库(参见Bandersnatch)或当仓库为特定平台提供补充工件(参见Piwheels)的情况下。
在这种情况下,被扩展的仓库或项目可能都不知道它们被扩展了或由谁扩展,因此这不能依赖于“扩展”仓库本身不存在的任何信息。
第二个用例是当项目希望发布到一个“主”仓库,然后拥有其他仓库为其他平台、GPU、CPU 等提供二进制文件时。目前,轮子标签不足以表达这些类型的二进制兼容性,因此希望依赖它们的项目被迫设置多个仓库,并让用户手动配置它们以获取适合其平台、GPU、CPU 等的正确二进制文件。
此用例类似于第一个用例,但重要的区别在于谁提供信息以及他们的信任级别。
当用户配置特定仓库(或依赖于默认仓库)时,他们所指的仓库没有歧义。仓库由 URL 标识,并且通过域名系统,URL 是全局唯一的标识符。这种缺乏歧义意味着安装程序可以假设仓库运营者是值得信赖的,并且可以信任他们提供的元数据,而无需验证它。
另一方面,鉴于安装程序在多个仓库中找到一个名称,则安装程序应该信任哪个仓库是不明确的。这种歧义意味着安装程序不能假设任何仓库中的项目所有者都是值得信赖的,并且需要验证它们是否确实是同一个项目,以及其中一个项目是否不是依赖混淆攻击。
如果没有某种方法让安装程序验证多个仓库之间的元数据,那么项目将被迫成为仓库运营者才能安全地支持此用例。这并不是一个特别错误的选择;但是,如果我们不提供一种方法让仓库让项目所有者安全地表达这种关系,那么他们将有动机让他们使用仓库运营者的元数据,这将重新引入最初的不安全性。
规范
此规范定义了简单仓库 API 版本 1.2 中的变化,添加了两个新的元数据项:“仓库“跟踪””和“备用位置”。
仓库“跟踪”元数据
为了使一个仓库能够托管一个旨在“扩展”托管在其他仓库中的项目的项目,此 PEP 允许扩展仓库通过添加项目和它正在扩展的仓库的 URL 来声明特定项目“跟踪”另一个仓库或仓库中的项目。
这在 JSON 中以键meta.tracks
的形式公开,在 HTML 中以项目特定 URL($root/$project/
)上的名为pypi:tracks
的元元素的形式公开。
使用此元数据时,必须保留一些关键属性
- 它**必须**受仓库运营者本身控制,而不是使用该仓库的任何个人发布者。
- “代码库操作员”也可能包括管理特定代码库的整体命名空间的任何人,在托管代码库服务等情况下可能出现这种情况,其中一个实体操作软件,但另一个实体拥有/管理该代码库的整个命名空间。
- 所有 URL **必须**表示与扩展代码库中项目相同的“项目”。
- 这并不意味着它们需要提供相同的文件。它们可以包含在不同平台上构建的二进制文件、应用了本地补丁的副本等,这都是有效的。这有意地留有模糊性,因为最终取决于用户对代码库及其操作员的期望,究竟什么是“相同”的项目。
- 它**必须**指向“拥有”命名空间的代码库,而不是另一个也跟踪该命名空间的代码库。
- 它**必须**指向具有完全相同名称(规范化后)的项目。
- 它**必须**指向该项目的实际 URL,而不是扩展代码库的基本 URL。
并非**要求**代码库中的每个名称都跟踪相同的代码库,或者它们都跟踪代码库。明确允许混合使用代码库,其中一些名称跟踪代码库,而一些名称不跟踪代码库。
JSON
{
"meta": {
"api-version": "1.2",
"tracks": ["https://pypi.ac.cn/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"]
},
"name": "holygrail",
"files": [
{
"filename": "holygrail-1.0.tar.gz",
"url": "https://example.com/files/holygrail-1.0.tar.gz",
"hashes": {"sha256": "...", "blake2b": "..."},
"requires-python": ">=3.7",
"yanked": "Had a vulnerability"
},
{
"filename": "holygrail-1.0-py3-none-any.whl",
"url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
"hashes": {"sha256": "...", "blake2b": "..."},
"requires-python": ">=3.7",
"dist-info-metadata": true
}
]
}
HTML
<!DOCTYPE html>
<html>
<head>
<meta name="pypi:repository-version" content="1.2">
<meta name="pypi:tracks" content="https://pypi.ac.cn/simple/holygrail/">
<meta name="pypi:tracks" content="https://test.pypi.org/simple/holygrail/">
</head>
<body>
<a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
<a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
</body>
</html>
“备用位置”元数据
为了使项目能够跨多个代码库扩展其命名空间,此 PEP 允许项目所有者声明其项目的“备用位置”列表。这在 JSON 中以键 alternate-locations
的形式公开,在 HTML 中以名为 pypi-alternate-locations
的元元素的形式公开,该元素可以多次使用。
使用此元数据时,**必须**遵守一些关键属性。
- 为了使此元数据值得信赖,在发现该项目的所有位置之间**必须**就备用位置达成一致。
- 使用备用位置时,客户端**必须**隐式地假设响应获取到的 url 包含在列表中。这意味着,如果您从
https://pypi.ac.cn/simple/foo/
获取,并且它具有值为["https://example.com/simple/foo/"]
的alternate-locations
元数据,那么您**必须**将其视为具有值为["https://example.com/simple/foo/", "https://pypi.ac.cn/simple/foo/"]
的值。 - 数组中元素的顺序没有任何特定含义。
当安装程序遇到使用备用位置元数据的项目时,**应该**认为所有命名的代码库都在跨多个代码库扩展相同的命名空间。
注意
此备用位置元数据是项目级元数据,而不是工件级元数据,这意味着它不包含在核心元数据规范中,而是每个代码库都必须为此提供配置选项(如果它们选择支持它)。
JSON
{
"meta": {
"api-version": "1.2"
},
"name": "holygrail",
"alternate-locations": ["https://pypi.ac.cn/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"],
"files": [
{
"filename": "holygrail-1.0.tar.gz",
"url": "https://example.com/files/holygrail-1.0.tar.gz",
"hashes": {"sha256": "...", "blake2b": "..."},
"requires-python": ">=3.7",
"yanked": "Had a vulnerability"
},
{
"filename": "holygrail-1.0-py3-none-any.whl",
"url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
"hashes": {"sha256": "...", "blake2b": "..."},
"requires-python": ">=3.7",
"dist-info-metadata": true
}
]
}
HTML
<!DOCTYPE html>
<html>
<head>
<meta name="pypi:repository-version" content="1.2">
<meta name="pypi:alternate-locations" content="https://pypi.ac.cn/simple/holygrail/">
<meta name="pypi:alternate-locations" content="https://test.pypi.org/simple/holygrail/">
</head>
<body>
<a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
<a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
</body>
</html>
建议
本节是非规范性的;它为安装程序提供了关于如何解释此元数据的建议,PEP 认为此元数据在保护用户和最大程度地减少对现有工作流的影响之间提供了最佳权衡。这些建议不具有约束力,安装程序可以自由地忽略它们,或者根据它们在特定情况下的意义选择性地应用它们。
文件发现算法
注意
此算法是根据 pip 当前发现文件的方式编写的;其他安装程序可以根据自己的发现过程调整此算法。
目前,“标准”文件发现算法如下所示
- 生成所有已配置代码库中所有文件的列表。
- 过滤掉与锁定文件或需求文件中已知哈希不匹配的任何文件。
- 过滤掉与当前平台、Python 版本等不匹配的任何文件。
- 将该文件列表传递给解析器,解析器将尝试从这些文件中解析出“最佳”匹配,而不管它来自哪个代码库。
建议安装程序更改其文件发现算法以考虑新的元数据,并改为执行以下操作
- 生成所有已配置代码库中所有文件的列表。
- 过滤掉与锁定文件或需求文件中已知哈希不匹配的任何文件。
- 如果最终用户已明确指示安装程序从特定代码库获取项目,则过滤掉所有其他代码库并跳到步骤 5。
- 查看发现的文件是否跨越多个代码库;如果是,则确定“跟踪”或“备用位置”元数据是否允许安全地将发现文件的所有代码库合并在一起。如果该元数据**不允许**这样做,则生成错误,否则继续。
- **注意:**这仅适用于远程代码库;存在于本地文件系统上的代码库**应该**始终被隐式地允许合并到任何远程代码库中。
- 过滤掉与当前平台、Python 版本等不匹配的任何文件。
- 将该文件列表传递给解析器,解析器将尝试从这些文件中解析出“最佳”匹配,而不管它来自哪个代码库。
这有点微妙,但建议中的关键事项是
- 使用包含“有效”工件特定哈希的锁定文件或需求文件的用户被认为本质上受这些哈希的保护,因为其余这些建议将在哈希生成期间适用。因此,我们预先过滤掉未知的哈希。
- 如果用户已明确指示安装程序要从特定一组代码库获取项目,则没有理由质疑这一点,我们假设他们已确保安全合并这些命名空间。
- 如果相关项目仅来自单个代码库,则不存在依赖关系混淆的可能性,因此除了允许之外,没有理由执行任何操作。
- 在根据平台、Python 版本等过滤之前,我们检查此 PEP 中的元数据,因为我们不希望仅在某些平台、Python 版本等上显示的错误。
- 如果没有指示我们合并命名空间是安全的,我们拒绝隐式地假设它是安全的,而是生成错误。
- 否则,我们合并命名空间,并继续执行。
此算法确保安装程序永远不会假设两个不同的命名空间可以合并为一个,这在所有实际目的中都消除了任何类型的依赖关系混淆攻击的可能性,同时仍然以安全的方式在整个堆栈中提供权力,让人们能够明确声明何时这些不同的命名空间实际上是一个可以安全合并的逻辑命名空间。
上述算法主要是一个概念模型。实际上,该算法最终可能略有不同,以实现更好的隐私保护和更快的速度,或者甚至只是为了更好地适应特定的安装程序而进行调整。
最终用户的显式配置
此 PEP 避免规定或推荐安装程序允许最终用户配置他们希望从哪些代码库安装特定软件包的具体机制。但是,它确实建议安装程序确实提供某种机制供最终用户提供该配置,因为如果没有它,用户最终可能会遇到类似于 torchtriton
的情况下的拒绝服务情况,除非他们在外部解决命名空间冲突(获取一个代码库上的名称,建立处理合并的个人代码库等),否则它们将完全中断。
此配置还允许最终用户在默认行为安全之前的漫长过渡期间预先保护自己。
如何传达此信息
注意
此示例特定于 pip,并假设 pip 将如何选择实现此 PEP 的具体细节;它作为我们如何传达此更改的示例,而不是旨在限制 pip 或任何其他安装程序如何实现此更改。这最终可能成为沟通的实际依据,如果是这样,则需要编辑以确保准确性和清晰度。
本节应视为一篇完整的“帖子”,用于传达可以用于博客文章、电子邮件或讨论帖子的此更改。
存在一类长期存在的攻击,称为“依赖关系混淆”攻击,大致归结为预期获取软件包 A
的个人,但他们却获取了 B
。在 Python 中,这几乎总是由于最终用户配置了多个代码库,他们期望软件包 A
来自代码库 X
,但有人能够在代码库 Y
中发布与软件包 A
同名的软件包 B
。
今天有许多方法可以减轻这些攻击,但它们都需要最终用户明确地采取措施来保护自己,而不是使其本身安全。
为了保护 pip 的用户并防止此类攻击,我们将更改 pip 发现要安装的软件包的方式。
发生了什么变化?
当 pip 发现同一项目可从多个远程代码库获得时,默认情况下它将生成错误并拒绝继续,而不是猜测要从中安装的正确代码库。
原本发布到多个代码库的项目将能够安全地将其代码库“链接”在一起,以便 pip 在这些代码库一起使用时不会出错。
pip 的最终用户将能够明确定义一个或多个对特定项目有效的代码库,从而导致 pip 仅考虑该项目的这些代码库,并避免完全生成错误。
有关更多信息,请参阅待定。
谁受到影响?
如果以下情况,则从多个远程(例如,不在本地文件系统上)代码库安装的用户可能会受到 pip 错误而不是成功安装的影响
- 他们安装了一个项目,其中同一“名称”由多个远程代码库提供。
- 可从多个远程代码库获得的项目名称尚未使用定义的机制之一将这些代码库链接在一起。
- 调用 pip 的用户尚未使用定义的机制来明确控制哪些代码库对特定项目有效。
不使用多个远程代码库的用户根本不会受到影响,其中包括仅使用单个远程代码库以及本地文件系统“wheel house”的用户。
我需要做什么?
作为 pip 用户?
如果您仅使用单个远程代码库,则无需执行任何操作。
如果您使用多个远程代码库,可以通过在 pip 调用中添加 --use-feature=TBD
来选择加入新行为,以查看您的任何依赖项是否由多个远程代码库提供。如果是,您应该对其进行审核以确定原因以及最合适的补救措施。
一旦此行为成为默认行为,您可以通过在 pip 调用中添加 --use-deprecated=TBD
来暂时选择退出。
如果您正在使用未托管在公共代码库上的项目,但您仍然将公共代码库作为后备,请考虑使用代码库文件配置 pip 以明确指定该依赖项的来源,以防止在公共代码库中注册该名称导致 pip 对您报错。
作为项目所有者?
如果您仅将项目发布到单个代码库,则无需执行任何操作。
如果您将项目发布到多个旨在同时使用的代码库,请配置所有代码库以提供备用代码库元数据,以防止您的最终用户出现故障。
如果您将项目发布到单个代码库,但它通常与其他代码库一起使用,请考虑预先在这些代码库中注册您的名称,以防止第三方导致您的用户 pip install
调用开始失败。如果您的项目名称过于通用或代码库具有阻止防御性名称抢注的策略,则这可能不可用。
作为仓库运营者?
您需要确定您的存储库打算如何被最终用户使用,以及您希望他们如何使用它。
对于托管私有项目的私有存储库,建议您将用户依赖的公共项目镜像到自己的存储库中,注意不要让公共项目与私有项目合并,并告诉用户使用--index-url
选项仅使用您的存储库。
对于托管公共项目的公共存储库,您应该实现备用存储库机制,并允许这些项目的拥有者配置其项目可用的存储库列表,如果他们从多个存储库提供该项目。
对于“跟踪”另一个存储库但提供补充工件(例如为特定平台构建的轮子)的公共存储库,您应该为您的存储库实现“轨道”元数据。但是,此信息**绝不能**由将项目发布到您的存储库的最终用户设置。有关更多信息,请参阅待定。
被否决的想法
注意:其中一些内容对 pip 来说有点特殊,但任何不适用于 pip 的解决方案都不是一个特别有用的解决方案。
当文件列表相同则隐式允许镜像
如果每个存储库都返回完全相同的文件列表,那么可以安全地认为这些存储库是相同的命名空间并隐式地合并它们。这可能意味着镜像将被自动允许,而无需任何用户或存储库操作员的操作。
不幸的是,这有两个缺点使其不可取
- 它只解决了完全复制彼此的镜像的情况,而不是“跟踪”另一个存储库的存储库,这最终是一个更通用的解决方案。
- 即使在完全镜像的情况下,多个存储库相互镜像也是一个分布式系统,不会始终彼此完全一致,实际上是一个最终一致的系统。这意味着依赖于此隐式启发式工作的存储库将由于源存储库和镜像存储库之间的漂移而出现零星的故障。
提供一种机制来对仓库进行排序
提供某种机制来为存储库指定顺序,然后在找到第一个为该项目提供文件的存储库时短路发现算法是另一种可行的安全解决方案,前提是顺序正确指定。
但是,由于多种原因,此方案已被拒绝
- 我们已经花费了 15 年以上的时间教育用户,存储库的指定顺序没有意义,并且它们实际上具有未定义的顺序。很难对此进行回溯并开始说现在顺序很重要。
- 用户可以轻松地重新排列他们在单个位置中指定的存储库的顺序,但在从多个位置(环境变量、配置文件、requirements 文件、cli 参数)加载存储库时,顺序是硬编码到 pip 中的。虽然它将是一个确定性和有据可查的顺序,但没有理由假设它是用户希望其存储库定义的顺序,迫使他们扭曲其配置 pip 的方式,以便隐式排序最终成为正确的顺序。
- 上述问题可以通过提供一种显式声明顺序的方法来缓解,而不是隐式地使用它们定义的顺序;但是,这意味着除非用户进行一些显式配置,否则不会提供保护。
- 排序假设一个存储库总是优先于另一个存储库,而没有任何方法可以按项目为基础进行决定。
- 依赖排序很微妙;如果我查看存储库的排序,我无法预先知道或确保哪些名称将来自哪些存储库。我只能在那一刻知道哪些名称由哪些存储库提供。
- 依赖排序很脆弱。没有理由假设两个不同的存储库不会发生随机的命名冲突——如果我正在使用来自较低优先级存储库的库,然后较高级别存储库碰巧开始出现冲突的名称会发生什么情况?
- 在排序做错事的情况下,它会默默地这样做,不会向用户提供任何反馈。这是设计使然,因为它实际上不知道错误或正确的事情是什么,它只是希望顺序能给出正确的事情,如果它做到了,那么用户就会得到保护而不会有任何中断。但是,当它做错事时,用户会遇到来自 pip 的非常令人困惑的行为,它只是默默地安装了错误的东西。
这个想法有一个变体,它实际上说,真正导致问题的只是 PyPI 的开放注册的本质,所以如果我们将除“默认”存储库之外的所有存储库都视为同等优先级,然后将默认存储库视为较低优先级,那么我们将解决问题。
这确实在一定程度上改善了情况,但它与一般的排序思路存在许多相同的问题(尽管不是全部)。
它还假设 PyPI 或配置为“默认”的任何存储库都是唯一一个开放注册名称的存储库。但是,像Piwheels这样的项目存在,用户期望除了 PyPI 之外还使用这些项目,这些项目也实际上具有开放注册名称,因为它跟踪在 PyPI 上注册的任何名称。
依赖于仓库代理
一种可能的解决方案是,不要让安装程序解决这个问题,而是依赖于可以安全地智能合并多个存储库的存储库代理。这可以为具有复杂需求的人提供更好的体验,因为他们可以拥有专门针对问题空间的配置和功能。
但是,这已被拒绝,因为
- 它要求用户选择使用它们,除非我们还删除在安装程序中拥有多个存储库的功能,以迫使用户在需要多个存储库时使用存储库代理。
- 删除配置多个存储库的功能已被拒绝,因为它会对最终用户造成过大的干扰。
- 用户可能需要在不同的上下文中合并多个存储库的不同结果,或者可能需要合并不同的、互斥的存储库。这意味着他们实际上需要为每组唯一的选项设置多个存储库代理。
- 它要求用户维护基础设施,或者它要求在安装程序中添加功能以自动为每次调用启动一个存储库。
- 它实际上并没有改变需要对这些问题有一个解决方案的要求,它只是将实现的责任从安装程序转移到某个存储库代理,但在任何一种情况下,我们仍然需要一些东西来弄清楚如何合并这些不同的命名空间。
- 最终,大多数用户不想仅仅为了安全地与多个存储库交互而不得不建立一个存储库代理。
仅依赖于哈希检查
另一种可能的解决方案是依赖于哈希检查,因为启用了哈希检查后,用户无法获得他们不期望的工件;命名空间是否合并错误并不重要。
这当然是一个解决方案;不幸的是,它也存在一些使其无法使用的问题
- 它要求用户选择加入,因此用户在默认情况下仍然不受保护。
- 它要求用户做大量工作来管理他们的哈希值,这是大多数用户不太可能愿意做的。
- 当用户没有使用
requirements.txt
文件作为其依赖项的来源时,获得保护非常困难且冗长(这会影响构建时依赖项以及命令行提供的依赖项)。 - 它只是部分解决了问题,在某种程度上,它只是将问题的责任转移到了生成安装程序将使用的哈希值的任何系统。如果该系统不是人工手动验证哈希值,而它不太可能是这样,那么我们只是将如何合并这些命名空间的问题转移到了实现哈希值维护的任何工具。
要求所有项目都存在于“默认”仓库中
另一个想法是我们可以缩小--extra-index-url
的范围,使其仅支持的用途是引用默认存储库的补充存储库,有效地表明默认存储库定义了命名空间,并且每个附加存储库都只使用额外的包扩展它。
此实现大致要求项目**必须**在默认存储库中注册,以便任何其他存储库都能正常工作。
如果您以这种方式成功缩小了范围,那么这种方法确实有效,但最终它已被拒绝,因为
- 用户不太可能理解或接受这种缩减的范围,因此他们可能会尝试继续以现在不受支持的方式使用它。
- 由于范围现在缩小了,因此事实使情况变得复杂,那些具有被排除的工作流的用户除了设置存储库代理之外,没有任何其他选择,这需要他们以前不必做的基础设施和工作。
- 它假设“额外”存储库中的名称与默认存储库中的名称相同,它们就是同一个项目。如果我们从一个全新的生态系统中从头开始,那么也许我们可以从一开始就做出这个假设并使其生效,但要让生态系统适应这种变化将非常困难。
- 这是这种方法的一个根本问题;导致依赖关系混淆的根本问题是我们正在获取不同的命名空间并将它们展平为一个。这种方法本质上只是声明“好的”,并试图通过要求每个人注册他们的名称来减轻它。
- 由于上述假设,在额外存储库中的名称意外与默认存储库冲突的情况下,它将对这些用户似乎有效,但他们将处于依赖关系混淆的状态。
- 更糟糕的是,允许这种情况发生的人名所有者将完全不知道他们在为该用户扮演的角色,并且可能会删除他们的项目或将其移交给其他人,从而可能让他们无意中允许恶意用户接管它。
- 用户可能会尝试通过在他们的默认存储库中注册他们的名称作为防御性名称占用来恢复到工作状态。他们执行此操作的能力将取决于其默认存储库的具体策略,是否有人已经拥有该名称,是否过于通用等。在最佳情况下,它将导致无用的占位符项目,这些项目除了保护某个名称的内部使用之外没有任何其他用途。
迁移到全局唯一名称
存在此问题的主要原因是我们没有全局唯一的名称,我们有在多个命名空间下存在的本地唯一的名称,我们试图将它们合并到一个扁平的命名空间中。如果我们可以想出一种方法来拥有全局唯一的名称,我们可以避开整个问题。
这个想法已被拒绝,因为
- 生成全局唯一但安全的名称,并且对人类也有意义,这几乎是不可能完成的任务,除非搭便车某种集中式数据库。据我所知,唯一设法做到这一点的系统最终都搭便车于域名系统,并通过带有域等的 URL 来引用包。
- 即使我们想出一个获得全局唯一名称的机制,我们将其改造到我们已有几十年的系统中的能力实际上为零,除非将所有内容都烧毁并重新开始。我们可能做的最好的事情是声明所有非全局唯一名称都是 PyPI 域名上的隐式名称,并强迫所有使用非 PyPI 包的人重命名他们的包。
- 这将颠覆我们当前系统的许多核心假设和基本部分,甚至很难列出它们。
仅建议安装程序提供显式配置
出现的一个想法是,基本上只是实现显式配置,并且不对任何其他内容进行任何其他更改。映射策略的具体提案实际上激发了显式配置选项,并创建了一个看起来像这样的文件
{
"repositories": {
"PyTorch": ["https://download.pytorch.org/whl/nightly"],
"PyPI": ["https://pypi.ac.cn/simple"]
},
"mapping": [
{
"paths": ["torch*"],
"repositories": ["PyTorch"],
"terminating": true
},
{
"paths": ["*"],
"repositories": ["PyPI"]
}
]
}
建议使用显式配置将如何实现的决策权推给了每个安装程序,允许他们选择最适合其用户的方案。
最终,仅实施某种显式配置被否决,因为它本质上是选择加入的,因此无法保护那些最无法使用现有工具解决问题的普通用户;通过在显式配置 alongside 添加额外的保护措施,我们能够默认保护所有用户。
此外,仅依赖显式配置也意味着每个最终用户都必须一遍又一遍地解决相同的问题,即使是在 PyPI、Piwheels、PyTorch 等镜像的情况下。在每一种情况下,他们都必须坐在那里做出决定(或找到一些示例来进行盲目复制),才能确保安全。将额外功能添加到组合中,使我们能够集中化这些保护措施,同时仍然让高级最终用户能够完全控制自己的命运。
类似 npm 的作用域
有人建议类似 npm 实现的范围 最终可能会解决这个问题。最终,范围并没有改变这个问题的任何内容。据我所知,npm 中的范围不是全局唯一的,它们像无范围名称一样绑定到特定注册表。但是,范围确实提供了一种明显的机制来对相关项目进行分组,以及 npm.org 上的用户或组织声明整个范围的能力,这使得显式配置显着易于处理,因为您可以确保名称空间中有一小部分完全属于您,并且您可以轻松地编写一条规则,将整个范围分配给特定的非公共注册表。
不幸的是,它基本上变成了仅使用显式配置的想法的一个更容易的版本,这在 npm 中效果不错,因为人们使用自己的注册表并不常见,但在 Python 中,我们鼓励您这样做。
定义和标准化“显式配置”
本 PEP 建议安装程序具有一种机制来显式配置特定项目来自哪个存储库,但它没有定义该机制是什么。我们有意将其保持未定义,因为它与每个安装程序的 UX 密切相关,并且我们希望允许每个安装程序能够以他们认为适合其特定用例的方式公开该配置。
此外,当提出定义该机制的想法时,其他安装程序似乎都没有特别感兴趣让该机制为他们定义,这表明他们很乐意将其视为其 UX 的一部分。
最后,如果我们确实选择定义该机制,它应该有自己的 PEP,而不是将其作为本 PEP 中存储库 API 更改的一部分进行烘焙,如果我们最终决定要走标准化的道路,它可以成为未来的 PEP。
致谢
感谢 Trishank Kuppusamy 启动了导致本 PEP 的讨论,以及他的提案。
感谢 Paul Moore、Pradyun Gedam、Steve Dower 和 Trishank Kuppusamy 对本 PEP 中的想法提供早期反馈和讨论。
感谢 Jelle Zijlstra、C.A.M. Gerlach、Hugo van Kemenade 和 Stefano Rivera 对本 PEP 进行校对并改进其结构和质量。
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可证,以两者中更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0708.rst
最后修改时间:2024-08-17 01:15:14 GMT