Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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最终确定之前需要满足以下条件

  1. PyPI (Warehouse) 中实现此PEP,包括允许项目所有者设置跟踪数据所需的任何 UI 元素。
  2. 在除PyPI之外的至少一个仓库中实现此PEP,因为如果没有至少两个索引,就无法真正测试合并索引。
  3. 在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:spamexample.com:spam。目前Python安装工具的标准行为是隐式地将这些多个命名空间扁平化为一个包含所有命名空间文件的命名空间。

这种认为合并命名空间是预期的假设意味着,当不同仓库中相同名称的包由不同方编写时(例如在torchtriton案例中),依赖混淆攻击成为可能。

这使得问题特别棘手,因为没有“正确”的答案;既存在希望将两个仓库合并为一个命名空间的有效用例,也存在希望将两个仓库视为不同命名空间的有效用例。这意味着安装程序需要某种机制来确定何时应该合并多个仓库的命名空间,何时不应该,而不是一概而论的“总是合并”或“从不合并”规则。

此功能可以直接推给终端用户,因为最终终端用户是真正关心从哪个仓库安装什么包的人。然而,通过扩展仓库规范以允许仓库指示何时安全,即使项目自然跨越多个不同的命名空间,我们也可以让单个项目和仓库“默认工作”,同时保持安装程序默认安全的能力。

此PEP本身并不能解决依赖混淆攻击,但它确实提供了足够的信息,以便安装程序可以在不给其他有效且安全的用例造成太多附带损害的情况下防止它们。

基本原理

本PEP旨在实现的跨仓库合并名称有两种主要用例。

第一个用例是当一个仓库没有定义自己的名称,而是扩展了其他仓库中定义的名称。这通常发生在项目从一个仓库镜像到另一个仓库(参见Bandersnatch)或当一个仓库为特定平台提供补充构件(参见Piwheels)的情况下。

在这种情况下,被扩展的仓库或项目可能根本不知道它们正在被扩展或由谁扩展,因此这不能依赖于“扩展”仓库本身不存在的任何信息。

第二个用例是当项目想要发布到一个“主”仓库,然后有额外的仓库为额外的平台、GPU、CPU等提供二进制文件。目前wheel标签不足以表达这些类型的二进制兼容性,因此希望依赖它们的项目被迫设置多个仓库,并让其用户手动配置它们以获取其平台、GPU、CPU等的正确二进制文件。

这个用例类似于第一个,但使其成为独立用例的重要区别在于提供信息的人以及他们的信任级别。

当用户配置特定的仓库(或依赖默认仓库)时,他们所指的仓库是明确的。仓库由URL标识,通过域名系统,URL是全球唯一标识符。这种明确性意味着安装程序可以假定仓库运营者是值得信赖的,并且可以信任他们提供的元数据而无需验证。

另一方面,如果安装程序在多个仓库中找到一个名称,那么安装程序应该信任哪个仓库是模糊的。这种模糊性意味着安装程序不能假定任何一个仓库的项目所有者是值得信赖的,并且需要验证它们是否确实是同一个项目,而不是依赖混淆攻击。

如果安装程序无法验证多个仓库之间的元数据,项目将被迫成为仓库运营者才能安全地支持此用例。这并非一个特别错误的选择;然而,如果项目运营者无法通过仓库安全地表达这种关系,他们可能会被激励使用仓库运营者的元数据,这会重新引入最初的不安全性。

规范

本规范定义了简单仓库API 1.2版本中的更改,增加了两个新的元数据项:仓库“跟踪”和“替代位置”。

仓库“跟踪”元数据

为了使一个仓库能够托管旨在“扩展”托管在其他仓库的项目的项目,本PEP允许扩展仓库通过添加其正在扩展的项目和仓库的URL,声明特定项目“跟踪”另一个仓库或多个仓库中的项目。

这在JSON中以键meta.tracks的形式公开,在HTML中以项目特定URL($root/$project/)上的名为pypi:tracks的meta元素的形式公开。

使用此元数据时**必须**保留以下几个关键属性:

  • 它**必须**由仓库运营者自行控制,而不是由使用该仓库的任何个人发布者控制。
    • “仓库运营者”也可以包括管理特定仓库整体命名空间的任何人,这可能发生在托管仓库服务等情况下,其中一个实体运营软件,但另一个实体拥有/管理该仓库的整个命名空间。
  • 所有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的meta元素的形式公开,该元素可以多次使用。

使用此元数据时,**必须**遵守以下几个关键属性

  • 为了使此元数据可信,该项目在所有发现位置**必须**就替代位置达成一致。
  • 使用替代位置时,客户端**必须**隐式假定获取响应的 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 当前发现文件的方式编写的;其他安装程序可能会根据自己的发现过程进行调整。

目前,“标准”文件发现算法大致如下:

  1. 生成所有配置仓库中所有文件的列表。
  2. 过滤掉与锁定文件或 requirements 文件中的已知哈希不匹配的任何文件。
  3. 过滤掉与当前平台、Python 版本等不匹配的任何文件。
  4. 将该文件列表传递给解析器,解析器将尝试从这些文件中解析出“最佳”匹配项,而不管它来自哪个仓库。

建议安装程序更改其文件发现算法以考虑新的元数据,并改为执行以下操作:

  1. 生成所有配置仓库中所有文件的列表。
  2. 过滤掉与锁定文件或 requirements 文件中的已知哈希不匹配的任何文件。
  3. 如果终端用户已明确告知安装程序从特定仓库获取项目,则过滤掉所有其他仓库并跳至第5步。
  4. 查看发现的文件是否跨越多个仓库;如果跨越,则确定“跟踪”或“替代位置”元数据是否允许安全地合并发现文件的**所有**仓库。如果该元数据**不允许**,则生成错误,否则继续。
    • 注意: 这仅适用于*远程*仓库;本地文件系统上存在的仓库**应**始终隐式允许合并到任何远程仓库。
  5. 过滤掉与当前平台、Python 版本等不匹配的任何文件。
  6. 将该文件列表传递给解析器,解析器将尝试从这些文件中解析出“最佳”匹配项,而不管它来自哪个仓库。

这有些微妙,但建议中的关键点是:

  • 使用包含特定构件哈希的锁定文件或 requirements 文件的用户被认为受到这些哈希的保护,因为其余建议将在哈希生成期间应用。因此,我们首先过滤掉未知哈希。
  • 如果用户已明确告知安装程序希望从一组特定的仓库中获取项目,那么就没有理由质疑这一点,我们假定他们已确保安全合并这些命名空间。
  • 如果相关项目只来自一个仓库,则没有依赖混淆的可能性,因此除了允许之外,没有理由做任何事情。
  • 我们在根据平台、Python 版本等进行过滤之前检查此PEP中的元数据,因为我们不希望只在某些平台、Python 版本等上出现错误。
  • 如果没有什么告诉我们合并命名空间是安全的,我们拒绝隐式假定它是安全的,而是生成一个错误。
  • 否则,我们合并命名空间,然后继续。

此算法确保安装程序从不假定两个不同的命名空间可以扁平化为一个,这实际上消除了任何依赖混淆攻击的可能性,同时仍然以安全的方式为整个堆栈提供能力,允许人们明确声明这些不同的命名空间何时实际上是一个可以安全合并的逻辑命名空间。

上述算法主要是一个概念模型。实际上,该算法最终可能会略有不同,以便更注重隐私和更快,甚至只是为了更好地适应特定的安装程序。

终端用户的明确配置

本PEP避免规定或推荐安装程序允许终端用户配置特定包从哪个仓库安装的具体机制。然而,它确实建议安装程序提供**某种**机制供终端用户提供该配置,因为如果没有它,在像torchtriton这样的情况下,用户可能会陷入DoS(拒绝服务)境地,除非他们外部解决命名空间冲突(在一个仓库中删除名称,建立一个处理合并的个人仓库等),否则他们将完全无法正常工作。

此配置还允许终端用户在可能漫长的过渡期(直到默认行为安全)内提前保护自己。

如何传达此信息

注意

此示例特定于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选项仅使用您的仓库。

对于托管公共项目的公共仓库,您应该实现替代仓库机制,并允许这些项目的拥有者配置其项目可用的仓库列表(如果他们使其可用于多个仓库)。

对于“跟踪”另一个仓库但提供补充构件(例如为特定平台构建的wheel)的公共仓库,您应该为您的仓库实现“跟踪”元数据。但是,此信息**不得**由向您的仓库发布项目的终端用户设置。有关更多信息,请参阅待定。

被拒绝的想法

注意:其中一些对于pip来说有点具体,但任何不适用于pip的解决方案都不是一个特别有用的解决方案。

当文件列表相同时隐式允许镜像

如果每个仓库都返回完全相同的文件列表,那么可以安全地认为这些仓库是相同的命名空间并隐式合并它们。这可能意味着镜像将自动允许,而无需任何用户或仓库运营者的任何工作。

不幸的是,这有两个使其不受欢迎的缺点

  • 它只解决了完全相同的镜像情况,而不是“跟踪”另一个仓库的情况,而后者是一个更通用的解决方案。
  • 即使在完全镜像的情况下,多个仓库相互镜像是一个分布式系统,它们之间并不总是完全一致,实际上是一个最终一致的系统。这意味着依赖这种隐式启发式方法工作的仓库会因为源仓库和镜像仓库之间的漂移而出现零星故障。

提供一种机制来对仓库进行排序

提供某种机制来为仓库提供顺序,然后在发现第一个为该项目提供文件的仓库时短路发现算法是另一个可行的解决方案,如果顺序指定正确,则是安全的。

然而,这因多种原因被拒绝

  • 我们已经花了15年多的时间教育用户,指定仓库的顺序是无关紧要的,它们实际上具有未定义的顺序。现在要收回并开始说顺序很重要将是困难的。
  • 用户可以轻松地在单个位置重新排列他们指定的仓库顺序,但是当从多个位置(环境变量、配置文件、requirements文件、命令行参数)加载仓库时,顺序是硬编码在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"]
    }
  ]
}

建议采用明确配置,将如何实现这一决策推给每个安装程序,允许他们选择最适合其用户的方法。

最终,仅仅实现某种显式配置被拒绝了,因为它本质上是选择加入的,因此它不能保护那些最不具备使用现有工具解决问题的普通用户;通过在显式配置之外添加额外的保护措施,我们能够默认保护所有用户。

此外,仅依赖明确配置也意味着每个终端用户都必须反复解决相同的问题,即使在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进行了校对和改进了结构和质量。


来源:https://github.com/python/peps/blob/main/peps/pep-0708.rst

最后修改:2025-02-01 08:55:40 GMT