diff --git a/warehouse/accounts/models.py b/warehouse/accounts/models.py index 78fd2cd37169..2d36c029ab2e 100644 --- a/warehouse/accounts/models.py +++ b/warehouse/accounts/models.py @@ -112,6 +112,13 @@ class User(SitemapMixin, HasEvents, db.Model): "Macaroon", backref="user", cascade="all, delete-orphan", lazy=True ) + pending_oidc_providers = orm.relationship( + "PendingOIDCProvider", + backref="added_by", + cascade="all, delete-orphan", + lazy=True, + ) + @property def primary_email(self): primaries = [x for x in self.emails if x.primary] diff --git a/warehouse/migrations/versions/aa3a4757f33a_add_pending_oidc_provider_hierarchy.py b/warehouse/migrations/versions/aa3a4757f33a_add_pending_oidc_provider_hierarchy.py new file mode 100644 index 000000000000..e70c7ce8d426 --- /dev/null +++ b/warehouse/migrations/versions/aa3a4757f33a_add_pending_oidc_provider_hierarchy.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. +""" +add pending OIDC provider hierarchy + +Revision ID: aa3a4757f33a +Revises: 43bf0b6badcb +Create Date: 2022-11-18 22:19:55.133681 +""" + +import sqlalchemy as sa + +from alembic import op +from sqlalchemy.dialects import postgresql + +revision = "aa3a4757f33a" +down_revision = "43bf0b6badcb" + + +def upgrade(): + op.create_table( + "pending_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.Column("project_name", sa.String(), nullable=False), + sa.Column("added_by_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint( + ["added_by_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_pending_oidc_providers_added_by_id"), + "pending_oidc_providers", + ["added_by_id"], + unique=False, + ) + op.create_table( + "pending_github_oidc_providers", + sa.Column("repository_name", sa.String(), nullable=True), + sa.Column("repository_owner", sa.String(), nullable=True), + sa.Column("repository_owner_id", sa.String(), nullable=True), + sa.Column("workflow_filename", sa.String(), nullable=True), + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ["id"], + ["pending_oidc_providers.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "repository_name", + "repository_owner", + "workflow_filename", + name="_pending_github_oidc_provider_uc", + ), + ) + + +def downgrade(): + op.drop_table("pending_github_oidc_providers") + op.drop_index( + op.f("ix_pending_oidc_providers_added_by_id"), + table_name="pending_oidc_providers", + ) + op.drop_table("pending_oidc_providers") diff --git a/warehouse/oidc/models.py b/warehouse/oidc/models.py index 7488668817e1..1c3fe7511d78 100644 --- a/warehouse/oidc/models.py +++ b/warehouse/oidc/models.py @@ -70,23 +70,16 @@ class OIDCProviderProjectAssociation(db.Model): ) -class OIDCProvider(db.Model): - __tablename__ = "oidc_providers" +class OIDCProviderMixin: + """ + A mixin for common functionality between all OIDC providers, including + "pending" providers that don't correspond to an extant project yet. + """ + # Each hierarchy of OIDC providers (both `OIDCProvider` and + # `PendingOIDCProvider`) use a `discriminator` column for model + # polymorphism, but the two are not mutually polymorphic at the DB level. discriminator = Column(String) - projects = orm.relationship( - Project, - secondary=OIDCProviderProjectAssociation.__table__, # type: ignore - backref="oidc_providers", - ) - macaroons = orm.relationship( - Macaroon, backref="oidc_provider", cascade="all, delete-orphan", lazy=True - ) - - __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, all-signed-claims) -> bool`. @@ -161,28 +154,57 @@ def verify_claims(self, signed_claims: SignedClaims): @property def provider_name(self): # pragma: no cover - # Only concrete subclasses of OIDCProvider are constructed. + # Only concrete subclasses are constructed. return NotImplemented @property def provider_url(self): # pragma: no cover - # Only concrete subclasses of OIDCProvider are constructed. + # Only concrete subclasses are constructed. return NotImplemented -class GitHubProvider(OIDCProvider): - __tablename__ = "github_oidc_providers" - __mapper_args__ = {"polymorphic_identity": "github_oidc_providers"} - __table_args__ = ( - UniqueConstraint( - "repository_name", - "repository_owner", - "workflow_filename", - name="_github_oidc_provider_uc", - ), +class OIDCProvider(OIDCProviderMixin, db.Model): + __tablename__ = "oidc_providers" + + projects = orm.relationship( + Project, + secondary=OIDCProviderProjectAssociation.__table__, # type: ignore + backref="oidc_providers", + ) + macaroons = orm.relationship( + Macaroon, backref="oidc_provider", cascade="all, delete-orphan", lazy=True ) - id = Column(UUID(as_uuid=True), ForeignKey(OIDCProvider.id), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "oidc_providers", + "polymorphic_on": OIDCProviderMixin.discriminator, + } + + +class PendingOIDCProvider(OIDCProviderMixin, db.Model): + """ + A "pending" OIDC provider, i.e. one that's been registered by a user + but doesn't correspond to an existing PyPI project yet. + """ + + __tablename__ = "pending_oidc_providers" + + project_name = Column(String, nullable=False) + added_by_id = Column( + UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True + ) + + __mapper_args__ = { + "polymorphic_identity": "pending_oidc_providers", + "polymorphic_on": OIDCProviderMixin.discriminator, + } + + +class GitHubProviderMixin: + """ + Common functionality for both pending and concrete GitHub OIDC providers. + """ + repository_name = Column(String) repository_owner = Column(String) repository_owner_id = Column(String) @@ -237,3 +259,35 @@ def job_workflow_ref(self): def __str__(self): return f"{self.workflow_filename} @ {self.repository}" + + +class GitHubProvider(GitHubProviderMixin, OIDCProvider): + __tablename__ = "github_oidc_providers" + __mapper_args__ = {"polymorphic_identity": "github_oidc_providers"} + __table_args__ = ( + UniqueConstraint( + "repository_name", + "repository_owner", + "workflow_filename", + name="_github_oidc_provider_uc", + ), + ) + + id = Column(UUID(as_uuid=True), ForeignKey(OIDCProvider.id), primary_key=True) + + +class PendingGitHubProvider(GitHubProviderMixin, PendingOIDCProvider): + __tablename__ = "pending_github_oidc_providers" + __mapper_args__ = {"polymorphic_identity": "pending_github_oidc_providers"} + __table_args__ = ( + UniqueConstraint( + "repository_name", + "repository_owner", + "workflow_filename", + name="_pending_github_oidc_provider_uc", + ), + ) + + id = Column( + UUID(as_uuid=True), ForeignKey(PendingOIDCProvider.id), primary_key=True + )