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
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
19 changes: 13 additions & 6 deletions tests/unit/attestations/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -89,7 +93,6 @@ def test_parse_attestations_fails_no_publisher(self, db_request):
@pytest.mark.parametrize(
"publisher_factory",
[
GitLabPublisherFactory,
GooglePublisherFactory,
ActiveStatePublisherFactory,
],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/oidc/models/test_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 0 additions & 1 deletion warehouse/attestations/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 17 additions & 17 deletions warehouse/locale/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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\" "
Expand Down
9 changes: 9 additions & 0 deletions warehouse/oidc/models/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")}
Expand Down
7 changes: 7 additions & 0 deletions warehouse/templates/includes/file-details.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down