diff --git a/tests/common/db/oidc.py b/tests/common/db/oidc.py index 5f0aade6ced5..770a6382a774 100644 --- a/tests/common/db/oidc.py +++ b/tests/common/db/oidc.py @@ -97,9 +97,10 @@ class Meta: id = factory.Faker("uuid4", cast_to=None) sub = factory.Faker("pystr", max_chars=12) organization_id = factory.Faker("uuid4") - organization_url_name = factory.Faker("pystr", max_chars=12) + organization = factory.Faker("pystr", max_chars=12) project_id = factory.Faker("uuid4") - activestate_project_name = factory.Faker("pystr", max_chars=12) + project = factory.Faker("pystr", max_chars=12) + actor = factory.Faker("pystr", max_chars=12) actor_id = factory.Faker("uuid4") branch_id = factory.Faker("uuid4") @@ -109,11 +110,11 @@ class Meta: model = PendingActiveStatePublisher id = factory.Faker("uuid4", cast_to=None) - sub = factory.Faker("pystr", max_chars=12) project_name = factory.Faker("pystr", max_chars=12) organization_id = factory.Faker("uuid4") - organization_url_name = factory.Faker("pystr", max_chars=12) + organization = factory.Faker("pystr", max_chars=12) project_id = factory.Faker("uuid4") - activestate_project_name = factory.Faker("pystr", max_chars=12) + project = factory.Faker("pystr", max_chars=12) + actor = factory.Faker("pystr", max_chars=12) actor_id = factory.Faker("uuid4") added_by = factory.SubFactory(UserFactory) diff --git a/tests/unit/oidc/models/test_activestate.py b/tests/unit/oidc/models/test_activestate.py index 341c2c6318d2..431212dec609 100644 --- a/tests/unit/oidc/models/test_activestate.py +++ b/tests/unit/oidc/models/test_activestate.py @@ -25,17 +25,16 @@ PendingActiveStatePublisher, ) -ORG_ID = "00000000-0000-1000-8000-000000000000" -PROJECT_ID = "00000000-0000-1000-8000-000000000001" +ORG_URL_NAME = "fakeorg" +PROJECT_NAME = "fakeproject" ACTOR_ID = "00000000-0000-1000-8000-000000000002" -BRANCH_ID = "00000000-0000-1000-8000-000000000003" # This follows the format of the subject that ActiveState sends us. We don't # validate the format when verifying the JWT. That should happen when the # Publisher is configured. We just need to make sure that the subject matches # # Technically, the branch should only be present if a branch was provided in the JWT # claims -SUBJECT = "org:fake_org_id:project:fake_project_id:branch_id:fake_branch_id" +SUBJECT = "org:fake_org_id:project:fake_project_id" def test_lookup_strategies(): @@ -48,27 +47,29 @@ def test_lookup_strategies(): def new_signed_claims( sub: str = SUBJECT, - organization_id: str = ORG_ID, - org_url_name: str = "fakeorg", - project_id: str = PROJECT_ID, - project_name: str = "fakeproject", - project_path: str = "fakeorg/fakeproject", + actor: str = "fakeuser", actor_id: str = ACTOR_ID, + organization: str = ORG_URL_NAME, + org_id: str = "fakeorgid", + project: str = PROJECT_NAME, + project_id: str = "fakeprojectid", + project_path: str = "fakeorg/fakeproject", project_visibility: str = "public", branch_id: str | None = None, ) -> SignedClaims: - project_name = "fakeproject" - org_url_name = "fakeorg" claims = SignedClaims( { "sub": sub, - "organization_id": organization_id, - "organization_url_name": org_url_name, - "project_id": project_id, - "project_name": project_name, - "project_path": project_path, + "actor": actor, "actor_id": actor_id, + "organization_id": org_id, + "organization": organization, "project_visibility": project_visibility, + "project_id": project_id, + "project_path": project_path, + "project_name": project, + "builder": "fakebuilder", + "ingredient": "fakeingredient", } ) if branch_id: @@ -85,9 +86,7 @@ def test_publisher_name(self): def test_publisher_url(self): org_name = "fakeorg" project_name = "fakeproject" - publisher = ActiveStatePublisher( - organization_url_name=org_name, activestate_project_name=project_name - ) + publisher = ActiveStatePublisher(organization=org_name, project=project_name) assert ( publisher.publisher_url() @@ -97,9 +96,7 @@ def test_publisher_url(self): def test_stringifies_as_project_url(self): org_name = "fakeorg" project_name = "fakeproject" - publisher = ActiveStatePublisher( - organization_url_name=org_name, activestate_project_name=project_name - ) + publisher = ActiveStatePublisher(organization=org_name, project=project_name) assert ( str(publisher) @@ -109,8 +106,8 @@ def test_stringifies_as_project_url(self): def test_activestate_publisher_all_known_claims(self): assert ActiveStatePublisher.all_known_claims() == { # verifiable claims - "organization_id", - "project_id", + "organization", + "project", "actor_id", "sub", # optional verifiable claims @@ -125,7 +122,7 @@ def test_activestate_publisher_all_known_claims(self): "project_visibility", "project_name", "project_path", - "organization_url_name", + "organization", } def test_activestate_publisher_unaccounted_claims(self, monkeypatch): @@ -161,11 +158,11 @@ def test_activestate_publisher_unaccounted_claims(self, monkeypatch): @pytest.mark.parametrize( ("claim_to_drop", "valid"), [ - ("organization_id", False), - ("project_id", False), + ("organization", False), + ("project", False), ("actor_id", False), ("branch_id", True), - ("organization_url_name", True), + ("organization", True), ("project_visibility", True), ("project_name", True), ("project_path", True), @@ -176,8 +173,8 @@ def test_activestate_publisher_missing_claims( ): publisher = ActiveStatePublisher( sub=SUBJECT, - organization_id=ORG_ID, - project_id=PROJECT_ID, + organization=ORG_URL_NAME, + project=PROJECT_NAME, actor_id=ACTOR_ID, ) @@ -211,8 +208,8 @@ def test_activestate_publisher_missing_claims( @pytest.mark.parametrize( ("expect", "actual", "valid"), [ - (ORG_ID, ORG_ID, True), - (ORG_ID, PROJECT_ID, False), + (ORG_URL_NAME, ORG_URL_NAME, True), + (ORG_URL_NAME, PROJECT_NAME, False), ], ) def test_activestate_publisher_org_id_verified( @@ -220,46 +217,19 @@ def test_activestate_publisher_org_id_verified( ): publisher = ActiveStatePublisher( sub=SUBJECT, - organization_id=actual, - project_id=PROJECT_ID, + organization=actual, + project=PROJECT_NAME, actor_id=ACTOR_ID, ) - signed_claims = new_signed_claims(organization_id=expect) + signed_claims = new_signed_claims(organization=expect) assert publisher.verify_claims(signed_claims=signed_claims) is valid @pytest.mark.parametrize( ("expect", "actual", "valid"), [ - (BRANCH_ID, BRANCH_ID, True), - (BRANCH_ID, PROJECT_ID, False), - # If it's configured in the publisher, it must be present in the claim - (BRANCH_ID, None, False), - # If it's not configured in the publisher, we don't care what it is - # in the claim - (None, None, True), - (None, PROJECT_ID, True), - ], - ) - def test_activestate_publisher_branch_id_verified( - self, expect: str, actual: str, valid: bool - ): - publisher = ActiveStatePublisher( - sub=SUBJECT, - organization_id=ORG_ID, - project_id=PROJECT_ID, - actor_id=ACTOR_ID, - branch_id=expect, - ) - - signed_claims = new_signed_claims(branch_id=actual) - assert publisher.verify_claims(signed_claims=signed_claims) is valid - - @pytest.mark.parametrize( - ("expect", "actual", "valid"), - [ - (PROJECT_ID, PROJECT_ID, True), - (PROJECT_ID, ORG_ID, False), + (PROJECT_NAME, PROJECT_NAME, True), + (PROJECT_NAME, ORG_URL_NAME, False), ], ) def test_activestate_publisher_project_id_verified( @@ -267,19 +237,19 @@ def test_activestate_publisher_project_id_verified( ): publisher = ActiveStatePublisher( sub=SUBJECT, - organization_id=ORG_ID, - project_id=actual, + organization=ORG_URL_NAME, + project=actual, actor_id=ACTOR_ID, ) - signed_claims = new_signed_claims(project_id=expect) + signed_claims = new_signed_claims(project=expect) assert publisher.verify_claims(signed_claims=signed_claims) is valid @pytest.mark.parametrize( ("expect", "actual", "valid"), [ (ACTOR_ID, ACTOR_ID, True), - (ACTOR_ID, ORG_ID, False), + (ACTOR_ID, ORG_URL_NAME, False), ], ) def test_activestate_publisher_user_id_verified( @@ -287,8 +257,8 @@ def test_activestate_publisher_user_id_verified( ): publisher = ActiveStatePublisher( sub=SUBJECT, - organization_id=ORG_ID, - project_id=PROJECT_ID, + organization=ORG_URL_NAME, + project=PROJECT_NAME, actor_id=actual, ) @@ -300,32 +270,26 @@ def test_activestate_publisher_user_id_verified( [ # Both present: must match. ( - f"org:{ORG_ID}:project:{PROJECT_ID}", - f"org:{ORG_ID}:project:{PROJECT_ID}", - True, - ), - # Both present, with branch id: must match. - ( - f"org:{ORG_ID}:project:{PROJECT_ID}:branch_id:{BRANCH_ID}", - f"org:{ORG_ID}:project:{PROJECT_ID}:branch_id:{BRANCH_ID}", + f"org:{ORG_URL_NAME}:project:{PROJECT_NAME}", + f"org:{ORG_URL_NAME}:project:{PROJECT_NAME}", True, ), - # sub configured without branch id, claim has branch id: must fail. + # Wrong value, project, must fail. ( - f"org:{ORG_ID}:project:{PROJECT_ID}", - f"org:{ORG_ID}:project:{PROJECT_ID}:branch_id:{BRANCH_ID}", + f"org:{ORG_URL_NAME}:project:{PROJECT_NAME}", + f"org:{ORG_URL_NAME}:project:{ORG_URL_NAME}", False, ), - # sub configured with branch id, claim missing branch id: must fail. + # Wrong value, org_id, must fail. ( - f"org:{ORG_ID}:project:{PROJECT_ID}:branch_id:{BRANCH_ID}", - f"org:{ORG_ID}:project:{PROJECT_ID}", + f"org:{ORG_URL_NAME}:project:{PROJECT_NAME}", + f"org:{PROJECT_NAME}:project:{PROJECT_NAME}", False, ), - # Wrong format for sub to expect from ActiveState: must fail. + # Just nonsenes, must fail. ( - f"org:{ORG_ID}:project:{PROJECT_ID}", - f"org:{ORG_ID}:project:{ORG_ID}", + f"org:{ORG_URL_NAME}:project:{PROJECT_NAME}", + "Nonsense", False, ), ], @@ -343,8 +307,10 @@ def test_reify_does_not_exist_yet(self, db_request): assert ( db_request.db.query(ActiveStatePublisher) .filter_by( - organization_id=pending_publisher.organization_id, - sub=pending_publisher.sub, + organization=pending_publisher.organization, + project=pending_publisher.project, + actor_id=pending_publisher.actor_id, + actor=pending_publisher.actor, ) .one_or_none() is None @@ -353,16 +319,16 @@ def test_reify_does_not_exist_yet(self, db_request): assert isinstance(publisher, ActiveStatePublisher) assert pending_publisher in db_request.db.deleted - assert publisher.organization_id == pending_publisher.organization_id + assert publisher.organization == pending_publisher.organization assert publisher.sub == pending_publisher.sub def test_reify_already_exists(self, db_request): existing_publisher: ActiveStatePublisher = ActiveStatePublisherFactory.create() pending_publisher = PendingActiveStatePublisherFactory.create( - organization_id=existing_publisher.organization_id, - project_id=existing_publisher.project_id, - branch_id=existing_publisher.branch_id, - sub=existing_publisher.sub, + organization=existing_publisher.organization, + project=existing_publisher.project, + actor_id=existing_publisher.actor_id, + actor=existing_publisher.actor, ) publisher = pending_publisher.reify(db_request.db) diff --git a/tests/unit/oidc/test_utils.py b/tests/unit/oidc/test_utils.py index 0b59f213326b..b5c64e53b2f9 100644 --- a/tests/unit/oidc/test_utils.py +++ b/tests/unit/oidc/test_utils.py @@ -178,7 +178,7 @@ def test_find_publisher_by_issuer_activestate( signed_claims = { "sub": sub, "organization_id": organization_id, - "organization_url_name": "fakeOrg", + "organization": "fakeOrg", "project_id": project_id, "project_name": "fakername1", "project_path": "fakeOrg/fakername1", diff --git a/warehouse/migrations/versions/6234fc5fa2ad_alter_activestate_publisher_table_to_.py b/warehouse/migrations/versions/6234fc5fa2ad_alter_activestate_publisher_table_to_.py deleted file mode 100644 index df567fba9511..000000000000 --- a/warehouse/migrations/versions/6234fc5fa2ad_alter_activestate_publisher_table_to_.py +++ /dev/null @@ -1,144 +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. -""" -Alter activestate publisher table to have actor_id - -Revision ID: 6234fc5fa2ad -Revises: e8c7bb1b94c6 -Create Date: 2023-08-01 21:09:17.643241 -""" - -import sqlalchemy as sa - -from alembic import op - -revision = "6234fc5fa2ad" -down_revision = "e8c7bb1b94c6" - -# 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. -# -# By default, migrations cannot wait more than 4s on acquiring a lock -# and each individual statement cannot take more than 5s. This helps -# prevent situations where a slow migration takes the entire site down. -# -# If you need to increase this timeout for a migration, you can do so -# by adding: -# -# op.execute("SET statement_timeout = 5000") -# op.execute("SET lock_timeout = 4000") -# -# To whatever values are reasonable for this migration as part of your -# migration. - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column( - "activestate_oidc_publishers", - sa.Column("actor_id", sa.VARCHAR(), nullable=False), - ) - op.drop_constraint( - "_activestate_oidc_publisher_uc", "activestate_oidc_publishers", type_="unique" - ) - op.create_unique_constraint( - "_activestate_oidc_publisher_uc", - "activestate_oidc_publishers", - [ - "organization_id", - "organization_url_name", - "project_id", - "actor_id", - "branch_id", - ], - ) - op.drop_column("activestate_oidc_publishers", "user_id") - op.add_column( - "pending_activestate_oidc_publishers", - sa.Column("actor_id", sa.VARCHAR(), nullable=False), - ) - op.drop_constraint( - "_pending_activestate_oidc_publisher_uc", - "pending_activestate_oidc_publishers", - type_="unique", - ) - op.create_unique_constraint( - "_pending_activestate_oidc_publisher_uc", - "pending_activestate_oidc_publishers", - [ - "organization_id", - "organization_url_name", - "project_id", - "actor_id", - "branch_id", - ], - ) - op.drop_column("pending_activestate_oidc_publishers", "user_id") - op.create_index( - "project_name_ultranormalized", - "projects", - [sa.text("ultranormalize_name(name)")], - unique=False, - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index("project_name_ultranormalized", table_name="projects") - op.add_column( - "pending_activestate_oidc_publishers", - sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False), - ) - op.drop_constraint( - "_pending_activestate_oidc_publisher_uc", - "pending_activestate_oidc_publishers", - type_="unique", - ) - op.create_unique_constraint( - "_pending_activestate_oidc_publisher_uc", - "pending_activestate_oidc_publishers", - [ - "organization_id", - "organization_url_name", - "project_id", - "user_id", - "branch_id", - ], - ) - op.drop_column("pending_activestate_oidc_publishers", "actor_id") - op.add_column( - "activestate_oidc_publishers", - sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False), - ) - op.drop_constraint( - "_activestate_oidc_publisher_uc", "activestate_oidc_publishers", type_="unique" - ) - op.create_unique_constraint( - "_activestate_oidc_publisher_uc", - "activestate_oidc_publishers", - [ - "organization_id", - "organization_url_name", - "project_id", - "user_id", - "branch_id", - ], - ) - op.drop_column("activestate_oidc_publishers", "actor_id") - # ### end Alembic commands ### diff --git a/warehouse/migrations/versions/e8c7bb1b94c6_add_activestate_oidc_publisher.py b/warehouse/migrations/versions/bb4dbfafda38_add_activestate_oidc_publisher.py similarity index 64% rename from warehouse/migrations/versions/e8c7bb1b94c6_add_activestate_oidc_publisher.py rename to warehouse/migrations/versions/bb4dbfafda38_add_activestate_oidc_publisher.py index 7c7abf48f2bf..91117a03b76b 100644 --- a/warehouse/migrations/versions/e8c7bb1b94c6_add_activestate_oidc_publisher.py +++ b/warehouse/migrations/versions/bb4dbfafda38_add_activestate_oidc_publisher.py @@ -10,18 +10,19 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -add ActiveState OIDC publisher +Add ActiveState OIDC publisher -Revision ID: e8c7bb1b94c6 +Revision ID: bb4dbfafda38 Revises: 4a0276f260c7 -Create Date: 2023-06-22 22:57:53.695035 +Create Date: 2023-08-05 00:33:10.339771 """ import sqlalchemy as sa from alembic import op +from sqlalchemy.dialects import postgresql -revision = "e8c7bb1b94c6" +revision = "bb4dbfafda38" down_revision = "4a0276f260c7" # Note: It is VERY important to ensure that a migration does not lock for a @@ -51,53 +52,41 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.create_table( "activestate_oidc_publishers", - sa.Column("id", sa.UUID(), nullable=False), - sa.Column("sub", sa.VARCHAR(), nullable=False), - sa.Column("organization_id", sa.VARCHAR(), nullable=False), - sa.Column("organization_url_name", sa.VARCHAR(), nullable=False), - sa.Column("project_id", sa.VARCHAR(), nullable=False), - sa.Column("activestate_project_name", sa.VARCHAR(), nullable=False), - sa.Column("user_id", sa.VARCHAR(), nullable=False), - sa.Column("branch_id", sa.VARCHAR(), nullable=True), + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("organization", sa.VARCHAR(), nullable=False), + sa.Column("project", sa.VARCHAR(), nullable=False), + sa.Column("actor", sa.VARCHAR(), nullable=False), + sa.Column("actor_id", sa.VARCHAR(), nullable=False), + sa.Column("ingredient", sa.VARCHAR(), nullable=True), sa.ForeignKeyConstraint( ["id"], ["oidc_publishers.id"], ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( - "organization_id", - "organization_url_name", - "project_id", - "user_id", - "branch_id", - name="_activestate_oidc_publisher_uc", + "organization", "project", "actor_id", name="_activestate_oidc_publisher_uc" ), ) op.create_table( "pending_activestate_oidc_publishers", - sa.Column("id", sa.UUID(), nullable=False), - sa.Column("sub", sa.VARCHAR(), nullable=False), - sa.Column("organization_id", sa.VARCHAR(), nullable=False), - sa.Column("organization_url_name", sa.VARCHAR(), nullable=False), - sa.Column("project_id", sa.VARCHAR(), nullable=False), - sa.Column("activestate_project_name", sa.VARCHAR(), nullable=False), - sa.Column("user_id", sa.VARCHAR(), nullable=False), - sa.Column("branch_id", sa.VARCHAR(), nullable=True), + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("organization", sa.VARCHAR(), nullable=False), + sa.Column("project", sa.VARCHAR(), nullable=False), + sa.Column("actor", sa.VARCHAR(), nullable=False), + sa.Column("actor_id", sa.VARCHAR(), nullable=False), + sa.Column("ingredient", sa.VARCHAR(), nullable=True), sa.ForeignKeyConstraint( ["id"], ["pending_oidc_publishers.id"], ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( - "organization_id", - "organization_url_name", - "project_id", - "user_id", - "branch_id", + "organization", + "project", + "actor_id", name="_pending_activestate_oidc_publisher_uc", ), ) - op.drop_index("project_name_ultranormalized", table_name="projects") # ### end Alembic commands ### # Disable the ActiveState OIDC provider by default op.execute( @@ -115,12 +104,6 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_index( - "project_name_ultranormalized", - "projects", - [sa.text("ultranormalize_name(name)")], - unique=False, - ) op.drop_table("pending_activestate_oidc_publishers") op.drop_table("activestate_oidc_publishers") # ### end Alembic commands ### diff --git a/warehouse/oidc/models/activestate.py b/warehouse/oidc/models/activestate.py index af8065eb7a77..35ffa2fa5ccc 100644 --- a/warehouse/oidc/models/activestate.py +++ b/warehouse/oidc/models/activestate.py @@ -12,17 +12,18 @@ from typing import Any +import urllib.parse as urlparse from sqlalchemy import VARCHAR, Column, ForeignKey, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Query from warehouse.oidc.interfaces import SignedClaims +import warehouse.oidc.models._core as oidccore from warehouse.oidc.models._core import ( CheckClaimCallable, OIDCPublisher, PendingOIDCPublisher, - check_claim_binary, ) ACTIVESTATE_URL = "https://platform.activestate.com" @@ -31,12 +32,25 @@ # branch_id is optional, so we need to verify but it's ok if it's missing -def _check_branch_id_optional( +def _check_sub( ground_truth: str, signed_claim: str, _all_signed_claims: SignedClaims -): - if ground_truth: - return ground_truth == signed_claim - return True +) -> bool: + # We expect a string formatted as follows: + # repo:ORG/REPO[:OPTIONAL-STUFF] + # where :OPTIONAL-STUFF is a concatenation of other job context + # metadata. We currently lack the ground context to verify that + # additional metadata, so we limit our verification to just the ORG/REPO + # component. + + # Defensive: GitHub should never give us an empty subject. + if not signed_claim: + return False + + components = signed_claim.split(":") + if len(components) < 2: + return False + + return f"{components[0]}:{components[1]}" == ground_truth class ActiveStatePublisherMixin: @@ -44,54 +58,71 @@ class ActiveStatePublisherMixin: Common functionality for both pending and concrete ActiveState OIDC publishers. """ - sub = Column(VARCHAR, nullable=False) - organization_id = Column(VARCHAR, nullable=False) - organization_url_name = Column(VARCHAR, nullable=False) - project_id = Column(VARCHAR, nullable=False) - activestate_project_name = Column(VARCHAR, nullable=False) + organization = Column(VARCHAR, nullable=False) + project = Column(VARCHAR, nullable=False) + actor = Column(VARCHAR, nullable=False) + # actor_username is obstained from the user while configuring the publisher + # We'll make an api call to ActiveState to get the actor_id actor_id = Column(VARCHAR, nullable=False) - branch_id = Column(VARCHAR) + ingredient = Column(VARCHAR, nullable=True) __required_verifiable_claims__: dict[str, CheckClaimCallable[Any]] = { - "sub": check_claim_binary(str.__eq__), - "organization_id": check_claim_binary(str.__eq__), - "project_id": check_claim_binary(str.__eq__), - "actor_id": check_claim_binary(str.__eq__), + "sub": _check_sub, + "organization": oidccore.check_claim_binary(str.__eq__), + "project": oidccore.check_claim_binary(str.__eq__), + "actor_id": oidccore.check_claim_binary(str.__eq__), + "actor": oidccore.check_claim_binary(str.__eq__), + "ingredient": oidccore.check_claim_binary(str.__eq__), + # This is the name of the builder in the ActiveState Platform that + # publishes things to PyPI. + "builder": oidccore.check_claim_invariant("pypi-publisher"), } __optional_verifiable_claims__: dict[str, CheckClaimCallable[Any]] = { - "branch_id": _check_branch_id_optional, + "ingredient": oidccore.check_claim_binary(str.__eq__), } __unchecked_claims__ = { - "organization_url_name", + "organization_id", "project_visibility", - "project_name", + "project_id", "project_path", + "branch_id", + # Should we check that the pypi projectname matches this ingredient + # name? + "ingredient", } @staticmethod def __lookup_all__(klass, signed_claims: SignedClaims): return Query(klass).filter_by( - organization_id=signed_claims["organization_id"], - project_id=signed_claims["project_id"], + organization=signed_claims["organization"], + project=signed_claims["project"], actor_id=signed_claims["actor_id"], - sub=signed_claims["sub"], + actor=signed_claims["actor"], ) __lookup_strategies__ = [ __lookup_all__, ] + @property + def sub(self) -> str: + return f"org:{self.organization}:project:{self.project}" + @property def publisher_name(self) -> str: return "ActiveState" + @property + def activestate_project_path(self) -> str: + return f"{self.organization}/{self.project}" + def publisher_url(self, claims: SignedClaims | None = None) -> str: - return f"{ACTIVESTATE_URL}/{self.organization_url_name}/{self.activestate_project_name}" # noqa + return urlparse.urljoin(ACTIVESTATE_URL, self.activestate_project_path) def __str__(self) -> str: - return f"{ACTIVESTATE_URL}/{self.organization_url_name}/{self.activestate_project_name}" # noqa + return self.publisher_url() class ActiveStatePublisher(ActiveStatePublisherMixin, OIDCPublisher): @@ -99,11 +130,9 @@ class ActiveStatePublisher(ActiveStatePublisherMixin, OIDCPublisher): __mapper_args__ = {"polymorphic_identity": "activestate_oidc_publishers"} __table_args__ = ( UniqueConstraint( - "organization_id", - "organization_url_name", - "project_id", + "organization", + "project", "actor_id", - "branch_id", name="_activestate_oidc_publisher_uc", ), ) @@ -116,11 +145,9 @@ class PendingActiveStatePublisher(ActiveStatePublisherMixin, PendingOIDCPublishe __mapper_args__ = {"polymorphic_identity": "pending_activestate_oidc_publishers"} __table_args__ = ( UniqueConstraint( - "organization_id", - "organization_url_name", - "project_id", + "organization", + "project", "actor_id", - "branch_id", name="_pending_activestate_oidc_publisher_uc", ), ) @@ -138,18 +165,18 @@ def reify(self, session): maybe_publisher = ( session.query(ActiveStatePublisher) .filter( - ActiveStatePublisher.organization_id == self.organization_id, - ActiveStatePublisher.project_id == self.project_id, - ActiveStatePublisher.branch_id == self.branch_id, + ActiveStatePublisher.organization == self.organization, + ActiveStatePublisher.project == self.project, + ActiveStatePublisher.actor_id == self.actor_id, ) .one_or_none() ) publisher = maybe_publisher or ActiveStatePublisher( sub=self.sub, - organization_id=self.organization_id, - project_id=self.project_id, - branch_id=self.branch_id, + organization=self.organization, + project=self.project, + actor_id=self.actor_id, ) session.delete(self)