From 48ab12eb1033882f6726b5b3519c2889e9cad6bf Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Mon, 14 Feb 2022 15:39:58 -0500
Subject: [PATCH 01/78] warehouse/oidc: rough model skeleton
---
warehouse/oidc/models.py | 47 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 warehouse/oidc/models.py
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
new file mode 100644
index 000000000000..78679c9a24fe
--- /dev/null
+++ b/warehouse/oidc/models.py
@@ -0,0 +1,47 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from sqlalchemy import UUID, Column, ForeignKey, String
+
+from warehouse import db
+
+
+class OIDCProvider(db.Model):
+ __tablename__ = "oidc_providers"
+
+ discriminator = Column(String)
+
+ __mapper_args__ = {"polymorphic_on": discriminator}
+
+ def verify_claims(self, token):
+ return NotImplemented
+
+
+class GitHubProvider(OIDCProvider):
+ __tablename__ = "github_oidc_providers"
+ __mapper_args__ = {"polymorphic_identity": "GitHubProvider"}
+
+ id = Column(UUID(as_uuid=True), ForeignKey(OIDCProvider.id), primary_key=True)
+ repository_name = Column(String)
+ owner = Column(String)
+ owner_id = Column(String)
+ workflow_name = Column(String)
+
+ def repository(self):
+ return f"{self.owner}/{self.repository_name}"
+
+ def job_workflow_ref(self):
+ return f"{self.repository}/.github/workflows/{self.workflow_name}.yml"
+
+ def verify_claims(self, token):
+ return NotImplemented
From 7db2c2cd5fcf175d6f998cfff721329a1a439bd9 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Mon, 14 Feb 2022 15:46:07 -0500
Subject: [PATCH 02/78] warehouse/oidc: fix imports
---
warehouse/oidc/models.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index 78679c9a24fe..b83ab4fbd57b 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -11,7 +11,8 @@
# limitations under the License.
-from sqlalchemy import UUID, Column, ForeignKey, String
+from sqlalchemy import Column, ForeignKey, String
+from sqlalchemy.dialects.postgresql import UUID
from warehouse import db
From eab5e707ae3056a61abe31a5843133aee4d9e3ce Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Mon, 14 Feb 2022 15:46:15 -0500
Subject: [PATCH 03/78] warehouse/migrations: add migration for OIDC models
---
...6d5769_add_initial_oidc_provider_models.py | 59 +++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
diff --git a/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py b/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
new file mode 100644
index 000000000000..3bb00d647758
--- /dev/null
+++ b/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
@@ -0,0 +1,59 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Add initial OIDC provider models
+
+Revision ID: 47422b6d5769
+Revises: 29a8901a4635
+Create Date: 2022-02-14 20:45:29.641650
+"""
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+revision = '47422b6d5769'
+down_revision = '29a8901a4635'
+
+# Note: It is VERY important to ensure that a migration does not lock for a
+# long period of time and to ensure that each individual migration does
+# not break compatibility with the *previous* version of the code base.
+# This is because the migrations will be ran automatically as part of the
+# deployment process, but while the previous version of the code is still
+# up and running. Thus backwards incompatible changes must be broken up
+# over multiple migrations inside of multiple pull requests in order to
+# phase them in over multiple deploys.
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('oidc_providers',
+ sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False),
+ sa.Column('discriminator', sa.String(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('github_oidc_providers',
+ sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column('repository_name', sa.String(), nullable=True),
+ sa.Column('owner', sa.String(), nullable=True),
+ sa.Column('owner_id', sa.String(), nullable=True),
+ sa.Column('workflow_name', sa.String(), nullable=True),
+ sa.ForeignKeyConstraint(['id'], ['oidc_providers.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('github_oidc_providers')
+ op.drop_table('oidc_providers')
+ # ### end Alembic commands ###
From 09d0966db447e9a6064235fe96232b5a99560718 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Mon, 14 Feb 2022 15:52:35 -0500
Subject: [PATCH 04/78] warehouse/migrations: reformat
---
...6d5769_add_initial_oidc_provider_models.py | 46 ++++++++++++-------
1 file changed, 29 insertions(+), 17 deletions(-)
diff --git a/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py b/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
index 3bb00d647758..0fa9c2bebb09 100644
--- a/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
+++ b/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
@@ -17,12 +17,13 @@
Create Date: 2022-02-14 20:45:29.641650
"""
-from alembic import op
import sqlalchemy as sa
+
+from alembic import op
from sqlalchemy.dialects import postgresql
-revision = '47422b6d5769'
-down_revision = '29a8901a4635'
+revision = "47422b6d5769"
+down_revision = "29a8901a4635"
# Note: It is VERY important to ensure that a migration does not lock for a
# long period of time and to ensure that each individual migration does
@@ -33,27 +34,38 @@
# over multiple migrations inside of multiple pull requests in order to
# phase them in over multiple deploys.
+
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.create_table('oidc_providers',
- sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False),
- sa.Column('discriminator', sa.String(), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "oidc_providers",
+ sa.Column(
+ "id",
+ postgresql.UUID(as_uuid=True),
+ server_default=sa.text("gen_random_uuid()"),
+ nullable=False,
+ ),
+ sa.Column("discriminator", sa.String(), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('github_oidc_providers',
- sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
- sa.Column('repository_name', sa.String(), nullable=True),
- sa.Column('owner', sa.String(), nullable=True),
- sa.Column('owner_id', sa.String(), nullable=True),
- sa.Column('workflow_name', sa.String(), nullable=True),
- sa.ForeignKeyConstraint(['id'], ['oidc_providers.id'], ),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "github_oidc_providers",
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("repository_name", sa.String(), nullable=True),
+ sa.Column("owner", sa.String(), nullable=True),
+ sa.Column("owner_id", sa.String(), nullable=True),
+ sa.Column("workflow_name", sa.String(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["id"],
+ ["oidc_providers.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('github_oidc_providers')
- op.drop_table('oidc_providers')
+ op.drop_table("github_oidc_providers")
+ op.drop_table("oidc_providers")
# ### end Alembic commands ###
From 24b9eab3dc09d3206d560a87e3d4dc0ec15714e2 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Mon, 14 Feb 2022 16:52:12 -0500
Subject: [PATCH 05/78] warehouse/oidc: add basic verification logic
---
warehouse/oidc/interfaces.py | 9 ++++++-
warehouse/oidc/models.py | 9 +++++--
warehouse/oidc/services.py | 47 +++++++++++++++++++++++++++++++++---
3 files changed, 59 insertions(+), 6 deletions(-)
diff --git a/warehouse/oidc/interfaces.py b/warehouse/oidc/interfaces.py
index 4c1ea13e9297..513d5119e9d3 100644
--- a/warehouse/oidc/interfaces.py
+++ b/warehouse/oidc/interfaces.py
@@ -28,5 +28,12 @@ def get_key(key_id):
def verify(token):
"""
- Verify the given JWT.
+ Verify the given JWT's signature and basic claims, returning
+ a tuple of (valid, decoded) where `valid` indicates
+ the validity of the JWT and `decoded` is the decoded JWT,
+ or `None` if invalid.
+
+ This function **does not** verify the token's suitability
+ for a particular action; subsequent checks on the decoded token's
+ third party claims must be done to ensure that.
"""
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index b83ab4fbd57b..cb7a0406d1a2 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -24,7 +24,7 @@ class OIDCProvider(db.Model):
__mapper_args__ = {"polymorphic_on": discriminator}
- def verify_claims(self, token):
+ def verify_claims(self, signed_token):
return NotImplemented
@@ -44,5 +44,10 @@ def repository(self):
def job_workflow_ref(self):
return f"{self.repository}/.github/workflows/{self.workflow_name}.yml"
- def verify_claims(self, token):
+ def verify_claims(self, signed_token):
+ """
+ Given a JWT that has been successfully decoded (checked for a valid
+ signature and basic claims), verify it against the more specific
+ claims of this provider.
+ """
return NotImplemented
diff --git a/warehouse/oidc/services.py b/warehouse/oidc/services.py
index f84a116a6cf0..a9499c03e17b 100644
--- a/warehouse/oidc/services.py
+++ b/warehouse/oidc/services.py
@@ -12,11 +12,11 @@
import json
+import jwt
import redis
import requests
import sentry_sdk
-from jwt import PyJWK
from zope.interface import implementer
from warehouse.metrics.interfaces import IMetricsService
@@ -148,10 +148,51 @@ def get_key(self, key_id):
tags=[f"provider:{self.provider}", f"key_id:{key_id}"],
)
return None
- return PyJWK(keyset[key_id])
+ return jwt.PyJWK(keyset[key_id])
+
+ def _get_key_for_token(self, token):
+ """
+ Return a JWK suitable for verifying the given JWT.
+
+ The JWT is not verified at this point, and this step happens
+ prior to any verification.
+ """
+ unverified_header = jwt.get_unverified_header(token)
+ return self.get_key(unverified_header["kid"])
def verify(self, token):
- return NotImplemented
+ key = self._get_key_for_token(token)
+
+ try:
+ # NOTE: Many of the keyword arguments here are defaults, but we
+ # set them explicitly to assert the intended verification behavior.
+ valid_token = jwt.decode(
+ token,
+ key=key,
+ algorithms=["RS256"],
+ verify_signature=True,
+ # "require" only checks for the presence of these claims, not
+ # their validity. Each has a corresponding "verify_" kwarg
+ # that enforces their actual validity.
+ require=["iss", "iat", "nbf", "exp", "aud"],
+ verify_iss=True,
+ verify_iat=True,
+ verify_nbf=True,
+ verify_exp=True,
+ verify_aud=True,
+ issuer=self.issuer_url,
+ audience="pypi",
+ leeway=60,
+ )
+ return True, valid_token
+ except jwt.PyJWTError:
+ return False, None
+ except Exception as e:
+ # We expect pyjwt to only raise subclasses of PyJWTError, but
+ # we can't enforce this. Other exceptions indicate an abstraction
+ # leak, so we log them for upstream reporting.
+ sentry_sdk.capture_message(f"JWT verify raised generic error: {e}")
+ return False, None
class OIDCProviderServiceFactory:
From c56476f2d966791cc7c48b5df9c7d9b53e23dd11 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Mon, 14 Feb 2022 16:56:00 -0500
Subject: [PATCH 06/78] oidc/services: reduce clock skew leeway to 30s
---
warehouse/oidc/services.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/warehouse/oidc/services.py b/warehouse/oidc/services.py
index a9499c03e17b..cbe818f36789 100644
--- a/warehouse/oidc/services.py
+++ b/warehouse/oidc/services.py
@@ -182,7 +182,7 @@ def verify(self, token):
verify_aud=True,
issuer=self.issuer_url,
audience="pypi",
- leeway=60,
+ leeway=30,
)
return True, valid_token
except jwt.PyJWTError:
From 41a1ca05e3eca51f47a9767c80ae734efe183250 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Tue, 15 Feb 2022 13:44:19 -0500
Subject: [PATCH 07/78] warehouse/oidc: refactor claim verification
---
warehouse/oidc/interfaces.py | 2 +-
warehouse/oidc/models.py | 83 +++++++++++++++++++++++++++++++-----
warehouse/oidc/services.py | 2 +-
3 files changed, 75 insertions(+), 12 deletions(-)
diff --git a/warehouse/oidc/interfaces.py b/warehouse/oidc/interfaces.py
index 513d5119e9d3..33ffa0639d55 100644
--- a/warehouse/oidc/interfaces.py
+++ b/warehouse/oidc/interfaces.py
@@ -26,7 +26,7 @@ def get_key(key_id):
"""
pass
- def verify(token):
+ def verify_signature_only(token):
"""
Verify the given JWT's signature and basic claims, returning
a tuple of (valid, decoded) where `valid` indicates
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index cb7a0406d1a2..3b81aa8d4a7a 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -10,6 +10,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import sentry_sdk
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.dialects.postgresql import UUID
@@ -24,8 +25,55 @@ class OIDCProvider(db.Model):
__mapper_args__ = {"polymorphic_on": discriminator}
- def verify_claims(self, signed_token):
- return NotImplemented
+ # A map of claim names to "check" functions, each of which
+ # has the signature `check(ground-truth, signed-claim) -> bool`.
+ __verifiable_claims__ = dict()
+
+ # Claims that have already been verified during the JWT signature
+ # verification phase.
+ __preverified_claims__ = {
+ "iss",
+ "iat",
+ "nbf",
+ "exp",
+ "aud",
+ }
+
+ # Individual providers should explicitly override this set,
+ # indicating any custom claims that are known to be present but are
+ # not checked as part of verifying the JWT.
+ __unchecked_claims__ = set()
+
+ def verify_claims(self, signed_claims):
+ """
+ Given a JWT that has been successfully decoded (checked for a valid
+ signature and basic claims), verify it against the more specific
+ claims of this provider.
+ """
+
+ # Defensive programming: treat the absence of any claims to verify
+ # as a failure rather than trivially valid.
+ if not self.__verifiable_claims__:
+ return False
+
+ # All claims should be accounted for.
+ # The presence of an unaccounted claim is not an error, only a warning
+ # that the JWT payload has changed.
+ known_claims = self.__verifiable_claims__.keys().union(
+ self.__preverified_claims__, self.__unchecked_claims__
+ )
+ unaccounted_claims = known_claims.difference(signed_claims.keys())
+ if unaccounted_claims:
+ sentry_sdk.capture_message(
+ f"JWT for {self.__class__.__name__} has unaccounted claims: {unaccounted_claims}"
+ )
+
+ # Finally, perform the actual claim verification.
+ for claim_name, check in self.__verifiable_claims__.items():
+ if not check(self.getattr(claim_name), signed_claims[claim_name]):
+ return False
+
+ return True
class GitHubProvider(OIDCProvider):
@@ -38,16 +86,31 @@ class GitHubProvider(OIDCProvider):
owner_id = Column(String)
workflow_name = Column(String)
+ __verifiable_claims__ = {
+ "repository": str.__eq__,
+ "job_workflow_ref": str.startswith,
+ "actor": str.__eq__,
+ "workflow": str.__eq__,
+ }
+
+ __unchecked_claims__ = {
+ "jti",
+ "sub",
+ "ref",
+ "sha",
+ "run_id",
+ "run_number",
+ "run_attempt",
+ "head_ref",
+ "base_ref",
+ "event_name",
+ "ref_type",
+ }
+
+ @property
def repository(self):
return f"{self.owner}/{self.repository_name}"
+ @property
def job_workflow_ref(self):
return f"{self.repository}/.github/workflows/{self.workflow_name}.yml"
-
- def verify_claims(self, signed_token):
- """
- Given a JWT that has been successfully decoded (checked for a valid
- signature and basic claims), verify it against the more specific
- claims of this provider.
- """
- return NotImplemented
diff --git a/warehouse/oidc/services.py b/warehouse/oidc/services.py
index cbe818f36789..4b70193a2033 100644
--- a/warehouse/oidc/services.py
+++ b/warehouse/oidc/services.py
@@ -160,7 +160,7 @@ def _get_key_for_token(self, token):
unverified_header = jwt.get_unverified_header(token)
return self.get_key(unverified_header["kid"])
- def verify(self, token):
+ def verify_signature_only(self, token):
key = self._get_key_for_token(token)
try:
From 63d16a2397aaaf1a6bebb47732dcf747d62fcc42 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Tue, 15 Feb 2022 13:46:45 -0500
Subject: [PATCH 08/78] oidc/models: fill in missing properties
---
warehouse/oidc/models.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index 3b81aa8d4a7a..7532bb29f872 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -114,3 +114,11 @@ def repository(self):
@property
def job_workflow_ref(self):
return f"{self.repository}/.github/workflows/{self.workflow_name}.yml"
+
+ @property
+ def actor(self):
+ return self.owner
+
+ @property
+ def workflow(self):
+ return self.workflow_name
From 50549ec7f1bd10f7d03fde2212cc5344b4accc4a Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Tue, 15 Feb 2022 13:48:34 -0500
Subject: [PATCH 09/78] warehouse/migrations: remove original OIDC migration
Add many-many project-provider association.
---
...6d5769_add_initial_oidc_provider_models.py | 71 -------------------
warehouse/oidc/models.py | 20 +++++-
2 files changed, 19 insertions(+), 72 deletions(-)
delete mode 100644 warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
diff --git a/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py b/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
deleted file mode 100644
index 0fa9c2bebb09..000000000000
--- a/warehouse/migrations/versions/47422b6d5769_add_initial_oidc_provider_models.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-Add initial OIDC provider models
-
-Revision ID: 47422b6d5769
-Revises: 29a8901a4635
-Create Date: 2022-02-14 20:45:29.641650
-"""
-
-import sqlalchemy as sa
-
-from alembic import op
-from sqlalchemy.dialects import postgresql
-
-revision = "47422b6d5769"
-down_revision = "29a8901a4635"
-
-# Note: It is VERY important to ensure that a migration does not lock for a
-# long period of time and to ensure that each individual migration does
-# not break compatibility with the *previous* version of the code base.
-# This is because the migrations will be ran automatically as part of the
-# deployment process, but while the previous version of the code is still
-# up and running. Thus backwards incompatible changes must be broken up
-# over multiple migrations inside of multiple pull requests in order to
-# phase them in over multiple deploys.
-
-
-def upgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table(
- "oidc_providers",
- sa.Column(
- "id",
- postgresql.UUID(as_uuid=True),
- server_default=sa.text("gen_random_uuid()"),
- nullable=False,
- ),
- sa.Column("discriminator", sa.String(), nullable=True),
- sa.PrimaryKeyConstraint("id"),
- )
- op.create_table(
- "github_oidc_providers",
- sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
- sa.Column("repository_name", sa.String(), nullable=True),
- sa.Column("owner", sa.String(), nullable=True),
- sa.Column("owner_id", sa.String(), nullable=True),
- sa.Column("workflow_name", sa.String(), nullable=True),
- sa.ForeignKeyConstraint(
- ["id"],
- ["oidc_providers.id"],
- ),
- sa.PrimaryKeyConstraint("id"),
- )
- # ### end Alembic commands ###
-
-
-def downgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table("github_oidc_providers")
- op.drop_table("oidc_providers")
- # ### end Alembic commands ###
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index 7532bb29f872..6158f6c2f855 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -12,16 +12,34 @@
import sentry_sdk
-from sqlalchemy import Column, ForeignKey, String
+from sqlalchemy import Column, ForeignKey, String, orm
from sqlalchemy.dialects.postgresql import UUID
from warehouse import db
+from warehouse.packaging.models import Project
+
+
+class OIDCProviderProjectAssociation(db.Model):
+ __tablename__ = "oidc_provider_project_association"
+
+ oidc_provider_id = Column(
+ UUID(as_uuid=True),
+ ForeignKey("oidc_providers.id"),
+ nullable=False,
+ primary_key=True,
+ )
+ project_id = Column(
+ UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False, primary_key=True
+ )
class OIDCProvider(db.Model):
__tablename__ = "oidc_providers"
discriminator = Column(String)
+ projects = orm.relationship(
+ Project, secondary=OIDCProviderProjectAssociation, backref="oidc_providers"
+ )
__mapper_args__ = {"polymorphic_on": discriminator}
From f1d7162fc2d97510ddc85cff2be450f43ddfba5b Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Tue, 15 Feb 2022 16:12:02 -0500
Subject: [PATCH 10/78] warehouse: add OIDC migration, fix association
---
...4c444f_add_initial_oidc_provider_models.py | 68 +++++++++++++++++++
warehouse/oidc/models.py | 2 +-
2 files changed, 69 insertions(+), 1 deletion(-)
create mode 100644 warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py
diff --git a/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py b/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py
new file mode 100644
index 000000000000..5a51874140bc
--- /dev/null
+++ b/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py
@@ -0,0 +1,68 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Add initial OIDC provider models
+
+Revision ID: f345394c444f
+Revises: 29a8901a4635
+Create Date: 2022-02-15 21:11:41.693791
+"""
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+revision = 'f345394c444f'
+down_revision = '29a8901a4635'
+
+# Note: It is VERY important to ensure that a migration does not lock for a
+# long period of time and to ensure that each individual migration does
+# not break compatibility with the *previous* version of the code base.
+# This is because the migrations will be ran automatically as part of the
+# deployment process, but while the previous version of the code is still
+# up and running. Thus backwards incompatible changes must be broken up
+# over multiple migrations inside of multiple pull requests in order to
+# phase them in over multiple deploys.
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('oidc_providers',
+ sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False),
+ sa.Column('discriminator', sa.String(), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('github_oidc_providers',
+ sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column('repository_name', sa.String(), nullable=True),
+ sa.Column('owner', sa.String(), nullable=True),
+ sa.Column('owner_id', sa.String(), nullable=True),
+ sa.Column('workflow_name', sa.String(), nullable=True),
+ sa.ForeignKeyConstraint(['id'], ['oidc_providers.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('oidc_provider_project_association',
+ sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False),
+ sa.Column('oidc_provider_id', postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=False),
+ sa.ForeignKeyConstraint(['oidc_provider_id'], ['oidc_providers.id'], ),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
+ sa.PrimaryKeyConstraint('id', 'oidc_provider_id', 'project_id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('oidc_provider_project_association')
+ op.drop_table('github_oidc_providers')
+ op.drop_table('oidc_providers')
+ # ### end Alembic commands ###
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index 6158f6c2f855..f098ba05c9d5 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -38,7 +38,7 @@ class OIDCProvider(db.Model):
discriminator = Column(String)
projects = orm.relationship(
- Project, secondary=OIDCProviderProjectAssociation, backref="oidc_providers"
+ Project, secondary=OIDCProviderProjectAssociation.__table__, backref="oidc_providers"
)
__mapper_args__ = {"polymorphic_on": discriminator}
From 5479c8102926b6603560e2a97511b5a99ae2db19 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Tue, 15 Feb 2022 16:12:34 -0500
Subject: [PATCH 11/78] warehouse: reformat
---
...4c444f_add_initial_oidc_provider_models.py | 74 ++++++++++++-------
warehouse/oidc/models.py | 4 +-
2 files changed, 52 insertions(+), 26 deletions(-)
diff --git a/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py b/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py
index 5a51874140bc..bbea7edfbb31 100644
--- a/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py
+++ b/warehouse/migrations/versions/f345394c444f_add_initial_oidc_provider_models.py
@@ -17,12 +17,13 @@
Create Date: 2022-02-15 21:11:41.693791
"""
-from alembic import op
import sqlalchemy as sa
+
+from alembic import op
from sqlalchemy.dialects import postgresql
-revision = 'f345394c444f'
-down_revision = '29a8901a4635'
+revision = "f345394c444f"
+down_revision = "29a8901a4635"
# Note: It is VERY important to ensure that a migration does not lock for a
# long period of time and to ensure that each individual migration does
@@ -33,36 +34,59 @@
# over multiple migrations inside of multiple pull requests in order to
# phase them in over multiple deploys.
+
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.create_table('oidc_providers',
- sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False),
- sa.Column('discriminator', sa.String(), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "oidc_providers",
+ sa.Column(
+ "id",
+ postgresql.UUID(as_uuid=True),
+ server_default=sa.text("gen_random_uuid()"),
+ nullable=False,
+ ),
+ sa.Column("discriminator", sa.String(), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('github_oidc_providers',
- sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
- sa.Column('repository_name', sa.String(), nullable=True),
- sa.Column('owner', sa.String(), nullable=True),
- sa.Column('owner_id', sa.String(), nullable=True),
- sa.Column('workflow_name', sa.String(), nullable=True),
- sa.ForeignKeyConstraint(['id'], ['oidc_providers.id'], ),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "github_oidc_providers",
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("repository_name", sa.String(), nullable=True),
+ sa.Column("owner", sa.String(), nullable=True),
+ sa.Column("owner_id", sa.String(), nullable=True),
+ sa.Column("workflow_name", sa.String(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["id"],
+ ["oidc_providers.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
)
- op.create_table('oidc_provider_project_association',
- sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), nullable=False),
- sa.Column('oidc_provider_id', postgresql.UUID(as_uuid=True), nullable=False),
- sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=False),
- sa.ForeignKeyConstraint(['oidc_provider_id'], ['oidc_providers.id'], ),
- sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
- sa.PrimaryKeyConstraint('id', 'oidc_provider_id', 'project_id')
+ op.create_table(
+ "oidc_provider_project_association",
+ sa.Column(
+ "id",
+ postgresql.UUID(as_uuid=True),
+ server_default=sa.text("gen_random_uuid()"),
+ nullable=False,
+ ),
+ sa.Column("oidc_provider_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["oidc_provider_id"],
+ ["oidc_providers.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["project_id"],
+ ["projects.id"],
+ ),
+ sa.PrimaryKeyConstraint("id", "oidc_provider_id", "project_id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('oidc_provider_project_association')
- op.drop_table('github_oidc_providers')
- op.drop_table('oidc_providers')
+ op.drop_table("oidc_provider_project_association")
+ op.drop_table("github_oidc_providers")
+ op.drop_table("oidc_providers")
# ### end Alembic commands ###
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index f098ba05c9d5..19a68fb20889 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -38,7 +38,9 @@ class OIDCProvider(db.Model):
discriminator = Column(String)
projects = orm.relationship(
- Project, secondary=OIDCProviderProjectAssociation.__table__, backref="oidc_providers"
+ Project,
+ secondary=OIDCProviderProjectAssociation.__table__,
+ backref="oidc_providers",
)
__mapper_args__ = {"polymorphic_on": discriminator}
From 1e0f26c0f8dba319736ec16d380928b708cd918c Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Wed, 16 Feb 2022 14:09:34 -0500
Subject: [PATCH 12/78] warehouse: OIDC route/view skeleton work
---
warehouse/manage/views.py | 13 ++++++++++++
warehouse/routes.py | 7 +++++++
warehouse/templates/manage/oidc.html | 14 +++++++++++++
warehouse/templates/manage/settings.html | 26 ++++++++++++++++++++++++
4 files changed, 60 insertions(+)
create mode 100644 warehouse/templates/manage/oidc.html
diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py
index cdd564507eac..808fc2cf97bd 100644
--- a/warehouse/manage/views.py
+++ b/warehouse/manage/views.py
@@ -992,6 +992,19 @@ def manage_project_settings(project, request):
}
+@view_config(
+ route_name="manage.project.settings.oidc",
+ context=Project,
+ renderer="manage/oidc.html",
+ uses_session=True,
+ permission="manage:project",
+ has_translations=True,
+ require_reauth=True,
+)
+def manage_project_oidc(project, request):
+ return {"project": project}
+
+
def get_user_role_in_project(project, user, request):
return (
request.db.query(Role)
diff --git a/warehouse/routes.py b/warehouse/routes.py
index e9d6006af7ed..b0604d809b5e 100644
--- a/warehouse/routes.py
+++ b/warehouse/routes.py
@@ -229,6 +229,13 @@ def includeme(config):
traverse="/{project_name}",
domain=warehouse,
)
+ config.add_route(
+ "manage.project.settings.oidc",
+ "/manage/project/{project_name}/settings/oidc/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ )
config.add_route(
"manage.project.delete_project",
"/manage/project/{project_name}/delete_project/",
diff --git a/warehouse/templates/manage/oidc.html b/warehouse/templates/manage/oidc.html
new file mode 100644
index 000000000000..89780d9dc9ef
--- /dev/null
+++ b/warehouse/templates/manage/oidc.html
@@ -0,0 +1,14 @@
+{#
+ # Licensed under the Apache License, Version 2.0 (the "License");
+ # you may not use this file except in compliance with the License.
+ # You may obtain a copy of the License at
+ #
+ # http://www.apache.org/licenses/LICENSE-2.0
+ #
+ # Unless required by applicable law or agreed to in writing, software
+ # distributed under the License is distributed on an "AS IS" BASIS,
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ # See the License for the specific language governing permissions and
+ # limitations under the License.
+-#}
+{% extends "manage_project_base.html" %}
diff --git a/warehouse/templates/manage/settings.html b/warehouse/templates/manage/settings.html
index 218a4f17a6ae..6f3310fdd2cc 100644
--- a/warehouse/templates/manage/settings.html
+++ b/warehouse/templates/manage/settings.html
@@ -55,6 +55,32 @@
{% trans %}API tokens{% endtrans %}
{% endtrans %}
{% endif %}
+
+
{% trans %}OIDC providers{% endtrans %}
+
+ {% trans %}
+ OIDC providers provide a credential-free way to authenticate when uploading packages to PyPI.
+ {% endtrans %}
+
+
+
+ {% if project.oidc_providers %}
+
{% trans %}Current providers:{% endtrans %}
+
+ {% for provider in project.oidc_providers %}
+
{{ provider }}
+ {% endfor %}
+
+ {% else %}
+
{% trans %}No providers are currently configured.{% endtrans %}
From bf5859bb3b33d521f72a0ad0f528774cb7f49530 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Thu, 17 Feb 2022 18:13:30 -0500
Subject: [PATCH 13/78] warehouse: form, view logic for adding OIDC providers
---
warehouse/manage/views.py | 58 ++++++++++++++--
warehouse/oidc/forms.py | 80 ++++++++++++++++++++++
warehouse/oidc/models.py | 19 +++++-
warehouse/templates/manage/oidc.html | 84 +++++++++++++++++++++++-
warehouse/templates/manage/settings.html | 20 ++----
5 files changed, 238 insertions(+), 23 deletions(-)
create mode 100644 warehouse/oidc/forms.py
diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py
index 808fc2cf97bd..414fb7ef3fda 100644
--- a/warehouse/manage/views.py
+++ b/warehouse/manage/views.py
@@ -71,6 +71,8 @@
ProvisionWebAuthnForm,
SaveAccountForm,
)
+from warehouse.oidc.forms import GitHubProviderForm
+from warehouse.oidc.models import GitHubProvider
from warehouse.packaging.models import (
File,
JournalEntry,
@@ -992,17 +994,65 @@ def manage_project_settings(project, request):
}
-@view_config(
- route_name="manage.project.settings.oidc",
+@view_defaults(
context=Project,
+ route_name="manage.project.settings.oidc",
renderer="manage/oidc.html",
uses_session=True,
+ require_csrf=True,
+ require_methods=False,
permission="manage:project",
has_translations=True,
require_reauth=True,
)
-def manage_project_oidc(project, request):
- return {"project": project}
+class ManageOIDCProviderViews:
+ def __init__(self, project, request):
+ self.request = request
+ self.project = project
+
+ @property
+ def default_response(self):
+ return {
+ "project": self.project,
+ "github_provider_form": GitHubProviderForm(),
+ }
+
+ @view_config(request_method="GET")
+ def manage_project_oidc_providers(self):
+ return self.default_response
+
+ @view_config(request_method="POST")
+ def add_github_oidc_provider(self, _form_class=GitHubProviderForm):
+ form = _form_class(self.request.POST)
+
+ if form.validate():
+ provider = GitHubProvider(
+ repository_name=form.repository,
+ owner=form.owner,
+ owner_id=form.owner_id,
+ workflow_name=form.workflow_name.data,
+ )
+
+ self.request.db.add(provider)
+ self.project.oidc_providers.append(provider)
+
+ self.project.record_event(
+ tag="project:oidc:provider-added",
+ ip_address=self.request.remote_addr,
+ additional={
+ "provider": "github",
+ "repository": form.repository_slug.data,
+ "workflow": form.workflow_name.data,
+ },
+ )
+
+ self.request.session.flash(
+ f"Added {form.workflow_name.data} on {form.repository_slug.data} "
+ f"to {self.project.name}",
+ queue="success",
+ )
+
+ return {**self.default_response, "github_provider_form": form}
def get_user_role_in_project(project, user, request):
diff --git a/warehouse/oidc/forms.py b/warehouse/oidc/forms.py
new file mode 100644
index 000000000000..9890bf23c1b7
--- /dev/null
+++ b/warehouse/oidc/forms.py
@@ -0,0 +1,80 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+
+import requests
+import wtforms
+
+from warehouse import forms
+from warehouse.i18n import localize as _
+
+# This roughly matches the "owner/repo" convention used by GitHub.
+_VALID_GITHUB_OWNER_REPO_SLUG = re.compile(
+ r"^[a-zA-Z0-9][a-zA-Z0-9-]*/[a-zA-Z0-9-_.]+$"
+)
+
+
+class GitHubProviderForm(forms.Form):
+ repository_slug = wtforms.StringField(
+ validators=[
+ wtforms.validators.DataRequired(message="Specify repository slug"),
+ ]
+ )
+
+ workflow_name = wtforms.StringField(
+ validators=[wtforms.validators.DataRequired(message="Specify workflow name")]
+ )
+
+ def validate_repository_slug(self, field):
+ repository_slug = field.data
+ if not _VALID_GITHUB_OWNER_REPO_SLUG.fullmatch(repository_slug):
+ raise wtforms.validators.ValidationError(
+ _(
+ "The specified repository is invalid. Repositories must be "
+ "specified in owner/repo format."
+ )
+ )
+
+ owner, repository = repository_slug.split("/", 1)
+
+ # To actually validate the owner, we ask GitHub's API about them.
+ # We can't do this for the repository, since it might be private.
+ response = requests.get(
+ f"https://api.github.com/users/{owner}",
+ headers={"Accept": "application/vnd.github.v3+json"},
+ allow_redirects=True,
+ )
+
+ if response.status_code == 404:
+ raise wtforms.validators.ValidationError(
+ _("Unknown GitHub user or organization.")
+ )
+ elif not response.ok:
+ raise wtforms.validators.ValidationError(
+ _("Unexpected error from GitHub. Try again.")
+ )
+
+ owner_info = response.json()
+
+ # NOTE: Use the normalized owner name as provided by GitHub.
+ self.owner = owner_info["login"]
+ self.owner_id = owner_info["id"]
+ self.repository = repository
+
+ def validate_workflow_name(self, field):
+ workflow_name = field.data
+
+ if not (workflow_name.endswith(".yml") or workflow_name.endswith(".yaml")):
+ raise wtforms.validators.ValidationError(
+ _("Workflow name must end with .yml or .yaml")
+ )
diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py
index 19a68fb20889..e3e3ba5418f8 100644
--- a/warehouse/oidc/models.py
+++ b/warehouse/oidc/models.py
@@ -43,7 +43,10 @@ class OIDCProvider(db.Model):
backref="oidc_providers",
)
- __mapper_args__ = {"polymorphic_on": discriminator}
+ __mapper_args__ = {
+ "polymorphic_identity": "oidc_providers",
+ "polymorphic_on": discriminator,
+ }
# A map of claim names to "check" functions, each of which
# has the signature `check(ground-truth, signed-claim) -> bool`.
@@ -85,7 +88,8 @@ def verify_claims(self, signed_claims):
unaccounted_claims = known_claims.difference(signed_claims.keys())
if unaccounted_claims:
sentry_sdk.capture_message(
- f"JWT for {self.__class__.__name__} has unaccounted claims: {unaccounted_claims}"
+ f"JWT for {self.__class__.__name__} has unaccounted claims: "
+ f"{unaccounted_claims}"
)
# Finally, perform the actual claim verification.
@@ -95,10 +99,15 @@ def verify_claims(self, signed_claims):
return True
+ @property
+ def provider_name():
+ # Only concrete subclasses of OIDCProvider are constructed.
+ return NotImplemented
+
class GitHubProvider(OIDCProvider):
__tablename__ = "github_oidc_providers"
- __mapper_args__ = {"polymorphic_identity": "GitHubProvider"}
+ __mapper_args__ = {"polymorphic_identity": "github_oidc_providers"}
id = Column(UUID(as_uuid=True), ForeignKey(OIDCProvider.id), primary_key=True)
repository_name = Column(String)
@@ -127,6 +136,10 @@ class GitHubProvider(OIDCProvider):
"ref_type",
}
+ @property
+ def provider_name():
+ return "github"
+
@property
def repository(self):
return f"{self.owner}/{self.repository_name}"
diff --git a/warehouse/templates/manage/oidc.html b/warehouse/templates/manage/oidc.html
index 89780d9dc9ef..28b94cec2c5f 100644
--- a/warehouse/templates/manage/oidc.html
+++ b/warehouse/templates/manage/oidc.html
@@ -11,4 +11,86 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
-{% extends "manage_project_base.html" %}
+
+{% extends "manage_base.html" %}
+
+{% block title %}
+ {% trans %}OIDC provider management{% endtrans %}
+{% endblock %}
+
+{% block content %}
+{% if testPyPI %}
+{% set title = "TestPyPI" %}
+{% else %}
+{% set title = "PyPI" %}
+{% endif %}
+
+
+
+
{% trans %}OIDC provider management{% endtrans %}
+
+
+ {% trans trimmed %}
+ OIDC provides a flexible, credential-free mechanism for delegating
+ publishing authority for a PyPI package to a third party service,
+ like GitHub Actions.
+ {% endtrans %}
+
+
+
+ {% trans trimmed %}
+ PyPI projects can use trusted OIDC providers to automate their release
+ processes, without having to explicitly provision or manage API tokens.
+ {% endtrans %}
+
+
+
{% trans %}Add a new provider{% endtrans %}
+
+
GitHub
+
+
+
+
{% trans %}Manage current providers{% endtrans %}
+ {% if project.oidc_providers %}
+
+ {% for provider in project.oidc_providers %}
+
{{ provider }}
+ {% endfor %}
+
+ {% else %}
+
{% trans %}No providers are currently configured.{% endtrans %}
+ {% trans %}Publisher information{% endtrans %}:
+
+
{% trans %}Publisher name{% endtrans %}: {{ provider_name }}
+
{% trans %}Publisher specification{% endtrans %}: {{ provider_spec }}
+
+
+
{% trans %}
If you did not make this change and you think it was made maliciously, you can
diff --git a/warehouse/templates/email/oidc-provider-added/body.txt b/warehouse/templates/email/oidc-provider-added/body.txt
index e1e3f23c64ee..bdf0c8ac4b0e 100644
--- a/warehouse/templates/email/oidc-provider-added/body.txt
+++ b/warehouse/templates/email/oidc-provider-added/body.txt
@@ -20,6 +20,11 @@ PyPI user {{ username }} has added a new OpenID Connect publisher to a project
users and can create project releases automatically.
{% endtrans %}
+{% trans %}Publisher information{% endtrans %}:
+
+* {% trans %}Publisher name{% endtrans %}: {{ provider_name }}
+* {% trans %}Publisher specification{% endtrans %}: {{ provider_spec }}
+
{% trans %}
If you did not make this change and you think it was made maliciously, you can
remove it from the project via the "Publishing" tab on the project's page.
diff --git a/warehouse/templates/email/oidc-provider-removed/body.html b/warehouse/templates/email/oidc-provider-removed/body.html
index e33044d5cc2a..d90cedcb152e 100644
--- a/warehouse/templates/email/oidc-provider-removed/body.html
+++ b/warehouse/templates/email/oidc-provider-removed/body.html
@@ -22,6 +22,14 @@
{% endtrans %}
+
+ {% trans %}Publisher information{% endtrans %}:
+
+
{% trans %}Publisher name{% endtrans %}: {{ provider_name }}
+
{% trans %}Publisher specification{% endtrans %}: {{ provider_spec }}
+
+
+
{% trans %}
If you did not make this change and you think it was made maliciously, you can
diff --git a/warehouse/templates/email/oidc-provider-removed/body.txt b/warehouse/templates/email/oidc-provider-removed/body.txt
index e586473788f2..93e1606824c0 100644
--- a/warehouse/templates/email/oidc-provider-removed/body.txt
+++ b/warehouse/templates/email/oidc-provider-removed/body.txt
@@ -19,6 +19,11 @@ PyPI user {{ username }} has removed an OpenID Connect publisher from a project
({{ project_name }}) that you manage.
{% endtrans %}
+{% trans %}Publisher information{% endtrans %}:
+
+* {% trans %}Publisher name{% endtrans %}: {{ provider_name }}
+* {% trans %}Publisher specification{% endtrans %}: {{ provider_spec }}
+
{% trans %}
If you did not make this change and you think it was made maliciously, you can
check the "Security history" tab on the project's page.
From e090ad347abc05697788ec416182333dfe332313 Mon Sep 17 00:00:00 2001
From: William Woodruff
Date: Tue, 5 Apr 2022 11:44:37 -0400
Subject: [PATCH 78/78] warehouse: `make translations`
---
warehouse/locale/messages.pot | 100 ++++++++++++++++++----------------
1 file changed, 54 insertions(+), 46 deletions(-)
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index 845fa4d13d89..06b3b0ff8733 100644
--- a/warehouse/locale/messages.pot
+++ b/warehouse/locale/messages.pot
@@ -243,81 +243,81 @@ msgid ""
"again later."
msgstr ""
-#: warehouse/manage/views.py:1870
+#: warehouse/manage/views.py:1872
msgid "User '${username}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views.py:1881
+#: warehouse/manage/views.py:1883
msgid ""
"User '${username}' does not have a verified primary email address and "
"cannot be added as a ${role_name} for project"
msgstr ""
-#: warehouse/manage/views.py:1894
+#: warehouse/manage/views.py:1896
msgid "User '${username}' already has an active invite. Please try again later."
msgstr ""
-#: warehouse/manage/views.py:1952
+#: warehouse/manage/views.py:1954
msgid "Invitation sent to '${username}'"
msgstr ""
-#: warehouse/manage/views.py:1999
+#: warehouse/manage/views.py:2001
msgid "Could not find role invitation."
msgstr ""
-#: warehouse/manage/views.py:2010
+#: warehouse/manage/views.py:2012
msgid "Invitation already expired."
msgstr ""
-#: warehouse/manage/views.py:2034
+#: warehouse/manage/views.py:2036
msgid "Invitation revoked from '${username}'."
msgstr ""
#: warehouse/oidc/forms.py:32
-msgid "Specify GitHub owner (username or organization)"
+msgid "Specify GitHub repository owner (username or organization)"
msgstr ""
#: warehouse/oidc/forms.py:39
-msgid "Specify repository slug"
+msgid "Specify repository name"
msgstr ""
#: warehouse/oidc/forms.py:41
msgid "Invalid repository name"
msgstr ""
-#: warehouse/oidc/forms.py:47
-msgid "Specify workflow name"
+#: warehouse/oidc/forms.py:48
+msgid "Specify workflow filename"
msgstr ""
-#: warehouse/oidc/forms.py:75
+#: warehouse/oidc/forms.py:77
msgid "Unknown GitHub user or organization."
msgstr ""
-#: warehouse/oidc/forms.py:85
+#: warehouse/oidc/forms.py:87
msgid "GitHub has rate-limited this action. Try again in a few minutes."
msgstr ""
-#: warehouse/oidc/forms.py:95
+#: warehouse/oidc/forms.py:97
msgid "Unexpected error from GitHub. Try again."
msgstr ""
-#: warehouse/oidc/forms.py:102
+#: warehouse/oidc/forms.py:104
msgid "Unexpected timeout from GitHub. Try again in a few minutes."
msgstr ""
-#: warehouse/oidc/forms.py:114
+#: warehouse/oidc/forms.py:116
msgid "Invalid GitHub user or organization name."
msgstr ""
-#: warehouse/oidc/forms.py:128
+#: warehouse/oidc/forms.py:132
msgid "Workflow name must end with .yml or .yaml"
msgstr ""
-#: warehouse/oidc/forms.py:133
-msgid "Workflow name must be a basename, without directories"
+#: warehouse/oidc/forms.py:137
+msgid "Workflow filename must be a filename only, without directories"
msgstr ""
-#: warehouse/oidc/forms.py:142
+#: warehouse/oidc/forms.py:146
msgid "Provider must be specified by ID"
msgstr ""
@@ -896,9 +896,9 @@ msgstr ""
#: warehouse/templates/manage/account/recovery_codes-burn.html:70
#: warehouse/templates/manage/account/totp-provision.html:69
#: warehouse/templates/manage/account/webauthn-provision.html:44
-#: warehouse/templates/manage/publishing.html:86
-#: warehouse/templates/manage/publishing.html:98
-#: warehouse/templates/manage/publishing.html:110
+#: warehouse/templates/manage/publishing.html:85
+#: warehouse/templates/manage/publishing.html:97
+#: warehouse/templates/manage/publishing.html:109
#: warehouse/templates/manage/roles.html:170
#: warehouse/templates/manage/roles.html:182
#: warehouse/templates/manage/token.html:136
@@ -1343,6 +1343,21 @@ msgid ""
msgstr ""
#: warehouse/templates/email/oidc-provider-added/body.html:28
+#: warehouse/templates/email/oidc-provider-removed/body.html:26
+msgid "Publisher information"
+msgstr ""
+
+#: warehouse/templates/email/oidc-provider-added/body.html:30
+#: warehouse/templates/email/oidc-provider-removed/body.html:28
+msgid "Publisher name"
+msgstr ""
+
+#: warehouse/templates/email/oidc-provider-added/body.html:31
+#: warehouse/templates/email/oidc-provider-removed/body.html:29
+msgid "Publisher specification"
+msgstr ""
+
+#: warehouse/templates/email/oidc-provider-added/body.html:36
msgid ""
"\n"
" If you did not make this change and you think it was made maliciously, "
@@ -1352,8 +1367,8 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/email/oidc-provider-added/body.html:35
-#: warehouse/templates/email/oidc-provider-removed/body.html:33
+#: warehouse/templates/email/oidc-provider-added/body.html:43
+#: warehouse/templates/email/oidc-provider-removed/body.html:41
#, python-format
msgid ""
"\n"
@@ -1375,7 +1390,7 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/email/oidc-provider-removed/body.html:26
+#: warehouse/templates/email/oidc-provider-removed/body.html:34
msgid ""
"\n"
" If you did not make this change and you think it was made maliciously, "
@@ -2994,72 +3009,65 @@ msgstr ""
msgid "Add a new provider"
msgstr ""
-#: warehouse/templates/manage/publishing.html:73
+#: warehouse/templates/manage/publishing.html:72
#, python-format
msgid ""
"Read more about GitHub's OpenID Connect provider here."
msgstr ""
-#: warehouse/templates/manage/publishing.html:84
+#: warehouse/templates/manage/publishing.html:83
#: warehouse/templates/manage/roles.html:43
#: warehouse/templates/manage/roles.html:77
#: warehouse/templates/manage/roles.html:88
msgid "Owner"
msgstr ""
-#: warehouse/templates/manage/publishing.html:89
+#: warehouse/templates/manage/publishing.html:88
msgid "owner"
msgstr ""
-#: warehouse/templates/manage/publishing.html:96
+#: warehouse/templates/manage/publishing.html:95
msgid "Repository name"
msgstr ""
-#: warehouse/templates/manage/publishing.html:101
+#: warehouse/templates/manage/publishing.html:100
msgid "repository"
msgstr ""
-#: warehouse/templates/manage/publishing.html:108
+#: warehouse/templates/manage/publishing.html:107
msgid "Workflow name"
msgstr ""
-#: warehouse/templates/manage/publishing.html:113
+#: warehouse/templates/manage/publishing.html:112
msgid "workflow.yml"
msgstr ""
-#: warehouse/templates/manage/publishing.html:119
+#: warehouse/templates/manage/publishing.html:118
msgid "Add"
msgstr ""
-#: warehouse/templates/manage/publishing.html:123
+#: warehouse/templates/manage/publishing.html:122
msgid "Manage current providers"
msgstr ""
-#: warehouse/templates/manage/publishing.html:127
+#: warehouse/templates/manage/publishing.html:126
#, python-format
msgid "OpenID Connect publishers associated with %(project_name)s"
msgstr ""
-#: warehouse/templates/manage/publishing.html:131
+#: warehouse/templates/manage/publishing.html:130
msgid "Publisher"
msgstr ""
-#: warehouse/templates/manage/publishing.html:132
+#: warehouse/templates/manage/publishing.html:131
msgid "Specification"
msgstr ""
-#: warehouse/templates/manage/publishing.html:143
+#: warehouse/templates/manage/publishing.html:142
msgid "No publishers are currently configured."
msgstr ""
-#: warehouse/templates/manage/publishing.html:147
-msgid ""
-"\n"
-" OpenID Connect publishing isn't enabled yet! Come back soon.\n"
-" "
-msgstr ""
-
#: warehouse/templates/manage/release.html:18
#, python-format
msgid "Manage '%(project_name)s' – release version %(version)s"