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

Add support PEP-740 attestations for GitLab CI/CD #17125

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Changes from all 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
2 changes: 1 addition & 1 deletion requirements/main.in
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ redis>=2.8.0,<6.0.0
rfc3986
sentry-sdk
setuptools
pypi-attestations==0.0.16
pypi-attestations==0.0.17
sqlalchemy[asyncio]>=2.0,<3.0
stdlib-list
stripe
6 changes: 3 additions & 3 deletions requirements/main.txt
Original file line number Diff line number Diff line change
@@ -1779,9 +1779,9 @@ pyparsing==3.2.0 \
--hash=sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84 \
--hash=sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c
# via linehaul
pypi-attestations==0.0.16 \
--hash=sha256:1d816719b5067ef49ac47c7ae229bd03752c57f957daf95b19b2ba19a66220e4 \
--hash=sha256:cbd2b946fe160793606dee4516ef58ac5959456e69630a7f03e7de73aa7f2737
pypi-attestations==0.0.17 \
--hash=sha256:5936c0c69af4e31d69543d03c9809c53c3f1c12b7eed6d83fe1bc81bf6a58c2e \
--hash=sha256:5a8a6a89f146d97357284fb6f467ea095273cf385f2f62ce49ad70b0a2057841
# via -r requirements/main.in
pyqrcode==1.2.1 \
--hash=sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6 \
19 changes: 13 additions & 6 deletions tests/unit/attestations/test_services.py
Original file line number Diff line number Diff line change
@@ -40,8 +40,12 @@ class TestNullIntegrityService:
def test_interface_matches(self):
assert verifyClass(IIntegrityService, services.NullIntegrityService)

def test_build_provenance(self, db_request, dummy_attestation):
db_request.oidc_publisher = GitHubPublisherFactory.create()
@pytest.mark.parametrize(
"publisher_factory",
[GitHubPublisherFactory, GitLabPublisherFactory],
)
def test_build_provenance(self, db_request, dummy_attestation, publisher_factory):
db_request.oidc_publisher = publisher_factory.create()

file = FileFactory.create()
service = services.NullIntegrityService.create_service(None, db_request)
@@ -89,7 +93,6 @@ def test_parse_attestations_fails_no_publisher(self, db_request):
@pytest.mark.parametrize(
"publisher_factory",
[
GitLabPublisherFactory,
GooglePublisherFactory,
ActiveStatePublisherFactory,
],
@@ -267,7 +270,7 @@ def test_parse_attestations_succeeds(

@pytest.mark.parametrize(
"publisher_factory",
[GitHubPublisherFactory],
[GitHubPublisherFactory, GitLabPublisherFactory],
)
def test_build_provenance_succeeds(
self, metrics, db_request, publisher_factory, dummy_attestation
@@ -295,8 +298,12 @@ def test_build_provenance_succeeds(
]


def test_extract_attestations_from_request_empty_list(db_request):
db_request.oidc_publisher = GitHubPublisherFactory.create()
@pytest.mark.parametrize(
"publisher_factory",
[GitHubPublisherFactory, GitLabPublisherFactory],
)
def test_extract_attestations_from_request_empty_list(db_request, publisher_factory):
db_request.oidc_publisher = publisher_factory.create()
db_request.POST = {"attestations": json.dumps([])}

with pytest.raises(
19 changes: 19 additions & 0 deletions tests/unit/oidc/models/test_gitlab.py
Original file line number Diff line number Diff line change
@@ -710,6 +710,25 @@ def test_gitlab_publisher_verify_url(
)
assert publisher.verify_url(url) == expected

@pytest.mark.parametrize("environment", ["", "some-env"])
def test_gitlab_publisher_attestation_identity(self, environment):
publisher = gitlab.GitLabPublisher(
project="project",
namespace="group/subgroup",
workflow_filepath="workflow_filename.yml",
environment=environment,
)

identity = publisher.attestation_identity
assert identity is not None
assert identity.repository == publisher.project_path
assert identity.workflow_filepath == publisher.workflow_filepath

if not environment:
assert identity.environment is None
else:
assert identity.environment == publisher.environment


class TestPendingGitLabPublisher:
def test_reify_does_not_exist_yet(self, db_request):
1 change: 0 additions & 1 deletion warehouse/attestations/services.py
Original file line number Diff line number Diff line change
@@ -151,7 +151,6 @@ def parse_attestations(
artifact. Attestations are only allowed when uploading via a Trusted
Publisher, because a Trusted Publisher provides the identity that will be
used to verify the attestations.
Only GitHub Actions Trusted Publishers are supported.
"""

attestations = _extract_attestations_from_request(request)
34 changes: 17 additions & 17 deletions warehouse/locale/messages.pot
Original file line number Diff line number Diff line change
@@ -826,7 +826,7 @@ msgstr ""
#: warehouse/templates/base.html:321 warehouse/templates/base.html:331
#: warehouse/templates/base.html:344
#: warehouse/templates/includes/accounts/profile-callout.html:18
#: warehouse/templates/includes/file-details.html:94
#: warehouse/templates/includes/file-details.html:101
#: warehouse/templates/index.html:100 warehouse/templates/index.html:104
#: warehouse/templates/manage/account.html:228
#: warehouse/templates/manage/account.html:234
@@ -2707,51 +2707,51 @@ msgstr ""
msgid "Public profile"
msgstr ""

#: warehouse/templates/includes/file-details.html:27
#: warehouse/templates/includes/file-details.html:34
msgid "File details"
msgstr ""

#: warehouse/templates/includes/file-details.html:38
#: warehouse/templates/includes/file-details.html:45
#, python-format
msgid "Upload date: %(upload_time)s"
msgstr ""

#: warehouse/templates/includes/file-details.html:39
#: warehouse/templates/includes/file-details.html:46
#, python-format
msgid "Size: %(size)s"
msgstr ""

#: warehouse/templates/includes/file-details.html:40
#: warehouse/templates/includes/file-details.html:47
#, python-format
msgid "Tags: %(tags)s"
msgstr ""

#: warehouse/templates/includes/file-details.html:42
#: warehouse/templates/includes/file-details.html:49
#, python-format
msgid "Uploaded using Trusted Publishing? %(is_tp)s"
msgstr ""

#: warehouse/templates/includes/file-details.html:47
#: warehouse/templates/includes/file-details.html:54
#, python-format
msgid "Uploaded via: %(uploaded_via)s"
msgstr ""

#: warehouse/templates/includes/file-details.html:55
#: warehouse/templates/includes/file-details.html:62
#, python-format
msgid "Hashes for %(filename)s"
msgstr ""

#: warehouse/templates/includes/file-details.html:58
#: warehouse/templates/includes/file-details.html:65
msgid "Algorithm"
msgstr ""

#: warehouse/templates/includes/file-details.html:59
#: warehouse/templates/includes/file-details.html:66
msgid "Hash digest"
msgstr ""

#: warehouse/templates/includes/file-details.html:68
#: warehouse/templates/includes/file-details.html:77
#: warehouse/templates/includes/file-details.html:86
#: warehouse/templates/includes/file-details.html:75
#: warehouse/templates/includes/file-details.html:84
#: warehouse/templates/includes/file-details.html:93
#: warehouse/templates/manage/account.html:206
#: warehouse/templates/manage/account/recovery_codes-provision.html:58
#: warehouse/templates/manage/account/totp-provision.html:57
@@ -2761,9 +2761,9 @@ msgstr ""
msgid "Copy to clipboard"
msgstr ""

#: warehouse/templates/includes/file-details.html:69
#: warehouse/templates/includes/file-details.html:78
#: warehouse/templates/includes/file-details.html:87
#: warehouse/templates/includes/file-details.html:76
#: warehouse/templates/includes/file-details.html:85
#: warehouse/templates/includes/file-details.html:94
#: warehouse/templates/manage/account.html:207
#: warehouse/templates/manage/account/recovery_codes-provision.html:59
#: warehouse/templates/manage/account/totp-provision.html:58
@@ -2772,7 +2772,7 @@ msgstr ""
msgid "Copy"
msgstr ""

#: warehouse/templates/includes/file-details.html:94
#: warehouse/templates/includes/file-details.html:101
#, python-format
msgid ""
"<a href=\"%(href)s\" title=\"%(title)s\" target=\"_blank\" "
9 changes: 9 additions & 0 deletions warehouse/oidc/models/gitlab.py
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@

from typing import Any

from pypi_attestations import GitLabPublisher as GitLabIdentity, Publisher
from sqlalchemy import ForeignKey, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Query, mapped_column
@@ -258,6 +259,14 @@ def publisher_url(self, claims=None):
base = self.publisher_base_url
return f"{base}/commit/{claims['sha']}" if claims else base

@property
def attestation_identity(self) -> Publisher | None:
return GitLabIdentity(
repository=self.project_path,
workflow_filepath=self.workflow_filepath,
environment=self.environment if self.environment else None,
)

def stored_claims(self, claims=None):
claims = claims if claims else {}
return {"ref_path": claims.get("ref_path"), "sha": claims.get("sha")}
7 changes: 7 additions & 0 deletions warehouse/templates/includes/file-details.html
Original file line number Diff line number Diff line change
@@ -20,6 +20,13 @@
<code>{{ publ.workflow }}</code> on {{ publ.repository }}
</a>
</p>
{% elif publ.kind == "GitLab" %}
<p>
Publisher: <a href="https://gitlab.com/{{ publ.repository }}/blob/HEAD/{{ publ.workflow_filepath }}">
<i class="fa-brands fa-gitlab" aria-hidden="true"></i>
<code>{{ publ.workflow_filepath }}</code> on {{ publ.repository }}
</a>
</p>
{% endif %}
{%- endmacro %}