Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

warehouse: add initial pending OIDC provider models #12572

Merged
merged 5 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions warehouse/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
111 changes: 81 additions & 30 deletions warehouse/oidc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,11 @@ class OIDCProviderProjectAssociation(db.Model):
)


class OIDCProvider(db.Model):
__tablename__ = "oidc_providers"

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,
}
class OIDCProviderMixin:
"""
A mixin for common functionality between all OIDC providers, including
"pending" providers that don't correspond to an extant project yet.
"""

# A map of claim names to "check" functions, each of which
# has the signature `check(ground-truth, signed-claim, all-signed-claims) -> bool`.
Expand Down Expand Up @@ -161,28 +149,59 @@ 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"

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
)

id = Column(UUID(as_uuid=True), ForeignKey(OIDCProvider.id), primary_key=True)
__mapper_args__ = {
"polymorphic_identity": "oidc_providers",
"polymorphic_on": 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"

discriminator = Column(String)
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
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": 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)
Expand Down Expand Up @@ -237,3 +256,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",
),
)
Comment on lines +267 to +274
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging for review: I had to dupe this UniqueConstraint between GitHubProvider and PendingGitHubProvider, even though they're functionally equivalent, thanks to the unique name requirement. There might be a better way to do this, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is due to not having set any naming_conventions for this project in the global metadata setup

metadata = sqlalchemy.MetaData()

Alembic docs go into it here: https://alembic.sqlalchemy.org/en/latest/naming.html

I think we always named them manually, like here:

UniqueConstraint("label", "user_id", name="_user_security_keys_label_uc"),

So it might be a future refactoring to replace the manually-named constraints, but I’d approach that with some serious caution 😉


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
)