Skip to content

Commit 3d8d548

Browse files
committed
Add a Google trusted publisher
1 parent bb5fd18 commit 3d8d548

File tree

9 files changed

+387
-8
lines changed

9 files changed

+387
-8
lines changed

tests/unit/accounts/test_views.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3340,11 +3340,19 @@ def test_manage_publishing(self, metrics, monkeypatch):
33403340
monkeypatch.setattr(
33413341
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
33423342
)
3343+
pending_google_publisher_form_obj = pretend.stub()
3344+
pending_google_publisher_form_cls = pretend.call_recorder(
3345+
lambda *a, **kw: pending_google_publisher_form_obj
3346+
)
3347+
monkeypatch.setattr(
3348+
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
3349+
)
33433350

33443351
view = views.ManageAccountPublishingViews(request)
33453352

33463353
assert view.manage_publishing() == {
33473354
"pending_github_publisher_form": pending_github_publisher_form_obj,
3355+
"pending_google_publisher_form": pending_google_publisher_form_obj,
33483356
}
33493357

33503358
assert request.flags.disallow_oidc.calls == [pretend.call()]
@@ -3382,11 +3390,19 @@ def test_manage_publishing_admin_disabled(self, monkeypatch, pyramid_request):
33823390
monkeypatch.setattr(
33833391
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
33843392
)
3393+
pending_google_publisher_form_obj = pretend.stub()
3394+
pending_google_publisher_form_cls = pretend.call_recorder(
3395+
lambda *a, **kw: pending_google_publisher_form_obj
3396+
)
3397+
monkeypatch.setattr(
3398+
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
3399+
)
33853400

33863401
view = views.ManageAccountPublishingViews(pyramid_request)
33873402

33883403
assert view.manage_publishing() == {
33893404
"pending_github_publisher_form": pending_github_publisher_form_obj,
3405+
"pending_google_publisher_form": pending_google_publisher_form_obj,
33903406
}
33913407

33923408
assert pyramid_request.flags.disallow_oidc.calls == [pretend.call()]
@@ -3434,11 +3450,19 @@ def test_add_pending_github_oidc_publisher_admin_disabled(
34343450
monkeypatch.setattr(
34353451
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
34363452
)
3453+
pending_google_publisher_form_obj = pretend.stub()
3454+
pending_google_publisher_form_cls = pretend.call_recorder(
3455+
lambda *a, **kw: pending_google_publisher_form_obj
3456+
)
3457+
monkeypatch.setattr(
3458+
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
3459+
)
34373460

34383461
view = views.ManageAccountPublishingViews(pyramid_request)
34393462

34403463
assert view.add_pending_github_oidc_publisher() == {
34413464
"pending_github_publisher_form": pending_github_publisher_form_obj,
3465+
"pending_google_publisher_form": pending_google_publisher_form_obj,
34423466
}
34433467

34443468
assert pyramid_request.flags.disallow_oidc.calls == [
@@ -3490,11 +3514,19 @@ def test_add_pending_github_oidc_publisher_user_cannot_register(
34903514
monkeypatch.setattr(
34913515
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
34923516
)
3517+
pending_google_publisher_form_obj = pretend.stub()
3518+
pending_google_publisher_form_cls = pretend.call_recorder(
3519+
lambda *a, **kw: pending_google_publisher_form_obj
3520+
)
3521+
monkeypatch.setattr(
3522+
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
3523+
)
34933524

34943525
view = views.ManageAccountPublishingViews(pyramid_request)
34953526

34963527
assert view.add_pending_github_oidc_publisher() == {
34973528
"pending_github_publisher_form": pending_github_publisher_form_obj,
3529+
"pending_google_publisher_form": pending_google_publisher_form_obj,
34983530
}
34993531

35003532
assert pyramid_request.flags.disallow_oidc.calls == [
@@ -3889,11 +3921,19 @@ def test_delete_pending_oidc_publisher_admin_disabled(
38893921
monkeypatch.setattr(
38903922
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
38913923
)
3924+
pending_google_publisher_form_obj = pretend.stub()
3925+
pending_google_publisher_form_cls = pretend.call_recorder(
3926+
lambda *a, **kw: pending_google_publisher_form_obj
3927+
)
3928+
monkeypatch.setattr(
3929+
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
3930+
)
38923931

38933932
view = views.ManageAccountPublishingViews(pyramid_request)
38943933

38953934
assert view.delete_pending_oidc_publisher() == {
38963935
"pending_github_publisher_form": pending_github_publisher_form_obj,
3936+
"pending_google_publisher_form": pending_google_publisher_form_obj,
38973937
}
38983938

38993939
assert pyramid_request.flags.disallow_oidc.calls == [pretend.call()]

tests/unit/manage/test_views.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5837,6 +5837,7 @@ def test_manage_project_oidc_publishers(self, monkeypatch):
58375837
assert view.manage_project_oidc_publishers() == {
58385838
"project": project,
58395839
"github_publisher_form": view.github_publisher_form,
5840+
"google_publisher_form": view.google_publisher_form,
58405841
}
58415842

58425843
assert request.flags.disallow_oidc.calls == [pretend.call()]
@@ -5865,6 +5866,7 @@ def test_manage_project_oidc_publishers_admin_disabled(
58655866
assert view.manage_project_oidc_publishers() == {
58665867
"project": project,
58675868
"github_publisher_form": view.github_publisher_form,
5869+
"google_publisher_form": view.google_publisher_form,
58685870
}
58695871

58705872
assert pyramid_request.flags.disallow_oidc.calls == [pretend.call()]
@@ -5935,6 +5937,11 @@ def test_add_github_oidc_publisher_preexisting(self, metrics, monkeypatch):
59355937
lambda *a, **kw: github_publisher_form_obj
59365938
)
59375939
monkeypatch.setattr(views, "GitHubPublisherForm", github_publisher_form_cls)
5940+
google_publisher_form_obj = pretend.stub()
5941+
google_publisher_form_cls = pretend.call_recorder(
5942+
lambda *a, **kw: google_publisher_form_obj
5943+
)
5944+
monkeypatch.setattr(views, "GooglePublisherForm", google_publisher_form_cls)
59385945

59395946
view = views.ManageOIDCPublisherViews(project, request)
59405947
monkeypatch.setattr(
@@ -6025,6 +6032,11 @@ def test_add_github_oidc_publisher_created(self, metrics, monkeypatch):
60256032
lambda *a, **kw: github_publisher_form_obj
60266033
)
60276034
monkeypatch.setattr(views, "GitHubPublisherForm", github_publisher_form_cls)
6035+
google_publisher_form_obj = pretend.stub()
6036+
google_publisher_form_cls = pretend.call_recorder(
6037+
lambda *a, **kw: google_publisher_form_obj
6038+
)
6039+
monkeypatch.setattr(views, "GooglePublisherForm", google_publisher_form_cls)
60286040
monkeypatch.setattr(
60296041
views,
60306042
"send_trusted_publisher_added_email",
@@ -6129,6 +6141,12 @@ def test_add_github_oidc_publisher_already_registered_with_project(
61296141
"_lookup_owner",
61306142
lambda *a: {"login": "some-owner", "id": "some-owner-id"},
61316143
)
6144+
google_publisher_form_obj = pretend.stub()
6145+
google_publisher_form_cls = pretend.call_recorder(
6146+
lambda *a, **kw: google_publisher_form_obj
6147+
)
6148+
monkeypatch.setattr(views, "GooglePublisherForm", google_publisher_form_cls)
6149+
61326150
monkeypatch.setattr(
61336151
view, "_hit_ratelimits", pretend.call_recorder(lambda: None)
61346152
)
@@ -6139,6 +6157,7 @@ def test_add_github_oidc_publisher_already_registered_with_project(
61396157
assert view.add_github_oidc_publisher() == {
61406158
"project": project,
61416159
"github_publisher_form": view.github_publisher_form,
6160+
"google_publisher_form": view.google_publisher_form,
61426161
}
61436162
assert view.metrics.increment.calls == [
61446163
pretend.call(

warehouse/accounts/views.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,17 @@
7777
)
7878
from warehouse.events.tags import EventTag
7979
from warehouse.metrics.interfaces import IMetricsService
80-
from warehouse.oidc.forms import DeletePublisherForm
81-
from warehouse.oidc.forms.github import PendingGitHubPublisherForm
80+
from warehouse.oidc.forms import (
81+
DeletePublisherForm,
82+
PendingGitHubPublisherForm,
83+
PendingGooglePublisherForm,
84+
)
8285
from warehouse.oidc.interfaces import TooManyOIDCRegistrations
83-
from warehouse.oidc.models import PendingGitHubPublisher, PendingOIDCPublisher
86+
from warehouse.oidc.models import (
87+
PendingGitHubPublisher,
88+
PendingGooglePublisher,
89+
PendingOIDCPublisher,
90+
)
8491
from warehouse.organizations.interfaces import IOrganizationService
8592
from warehouse.organizations.models import OrganizationRole, OrganizationRoleType
8693
from warehouse.packaging.models import (
@@ -1467,6 +1474,10 @@ def __init__(self, request):
14671474
api_token=self.request.registry.settings.get("github.token"),
14681475
project_factory=self.project_factory,
14691476
)
1477+
self.pending_google_publisher_form = PendingGooglePublisherForm(
1478+
self.request.POST,
1479+
project_factory=self.project_factory,
1480+
)
14701481

14711482
@property
14721483
def _ratelimiters(self):
@@ -1502,6 +1513,7 @@ def _check_ratelimits(self):
15021513
def default_response(self):
15031514
return {
15041515
"pending_github_publisher_form": self.pending_github_publisher_form,
1516+
"pending_google_publisher_form": self.pending_google_publisher_form,
15051517
}
15061518

15071519
@view_config(request_method="GET")
@@ -1640,6 +1652,29 @@ def _add_pending_oidc_publisher(
16401652

16411653
return HTTPSeeOther(self.request.path)
16421654

1655+
@view_config(
1656+
request_method="POST",
1657+
request_param=PendingGooglePublisherForm.__params__,
1658+
)
1659+
def add_pending_google_oidc_publisher(self):
1660+
form = self.default_response["pending_google_publisher_form"]
1661+
return self._add_pending_oidc_publisher(
1662+
publisher_name="Google",
1663+
publisher_class=PendingGooglePublisher,
1664+
admin_flag=AdminFlagValue.DISALLOW_GOOGLE_OIDC,
1665+
form=form,
1666+
make_pending_publisher=lambda request, form: PendingGooglePublisher(
1667+
project_name=form.project_name.data,
1668+
added_by=request.user,
1669+
email=form.email.data,
1670+
sub=form.sub.data,
1671+
),
1672+
make_existence_filters=lambda form: dict(
1673+
email=form.email.data,
1674+
sub=form.sub.data,
1675+
),
1676+
)
1677+
16431678
@view_config(
16441679
request_method="POST",
16451680
request_param=PendingGitHubPublisherForm.__params__,

warehouse/manage/views/__init__.py

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,13 @@
9999
user_projects,
100100
)
101101
from warehouse.metrics.interfaces import IMetricsService
102-
from warehouse.oidc.forms import DeletePublisherForm
103-
from warehouse.oidc.forms.github import GitHubPublisherForm
102+
from warehouse.oidc.forms import (
103+
DeletePublisherForm,
104+
GitHubPublisherForm,
105+
GooglePublisherForm,
106+
)
104107
from warehouse.oidc.interfaces import TooManyOIDCRegistrations
105-
from warehouse.oidc.models import GitHubPublisher, OIDCPublisher
108+
from warehouse.oidc.models import GitHubPublisher, GooglePublisher, OIDCPublisher
106109
from warehouse.organizations.interfaces import IOrganizationService
107110
from warehouse.organizations.models import (
108111
OrganizationProject,
@@ -1135,6 +1138,7 @@ def __init__(self, project, request):
11351138
self.request.POST,
11361139
api_token=self.request.registry.settings.get("github.token"),
11371140
)
1141+
self.google_publisher_form = GooglePublisherForm(self.request.POST)
11381142

11391143
@property
11401144
def _ratelimiters(self):
@@ -1171,6 +1175,7 @@ def default_response(self):
11711175
return {
11721176
"project": self.project,
11731177
"github_publisher_form": self.github_publisher_form,
1178+
"google_publisher_form": self.google_publisher_form,
11741179
}
11751180

11761181
@view_config(request_method="GET")
@@ -1299,6 +1304,114 @@ def add_github_oidc_publisher(self):
12991304

13001305
return HTTPSeeOther(self.request.path)
13011306

1307+
@view_config(
1308+
request_method="POST",
1309+
request_param=GooglePublisherForm.__params__,
1310+
)
1311+
def add_google_oidc_publisher(self):
1312+
if self.request.flags.disallow_oidc(AdminFlagValue.DISALLOW_GOOGLE_OIDC):
1313+
self.request.session.flash(
1314+
self.request._(
1315+
"Google-based trusted publishing is temporarily disabled. "
1316+
"See https://pypi.org/help#admin-intervention for details."
1317+
),
1318+
queue="error",
1319+
)
1320+
return self.default_response
1321+
1322+
self.metrics.increment(
1323+
"warehouse.oidc.add_publisher.attempt", tags=["publisher:Google"]
1324+
)
1325+
1326+
try:
1327+
self._check_ratelimits()
1328+
except TooManyOIDCRegistrations as exc:
1329+
self.metrics.increment(
1330+
"warehouse.oidc.add_publisher.ratelimited", tags=["publisher:Google"]
1331+
)
1332+
return HTTPTooManyRequests(
1333+
self.request._(
1334+
"There have been too many attempted trusted publisher "
1335+
"registrations. Try again later."
1336+
),
1337+
retry_after=exc.resets_in.total_seconds(),
1338+
)
1339+
1340+
self._hit_ratelimits()
1341+
1342+
response = self.default_response
1343+
form = response["google_publisher_form"]
1344+
1345+
if not form.validate():
1346+
self.request.session.flash(
1347+
self.request._("The trusted publisher could not be registered"),
1348+
queue="error",
1349+
)
1350+
return response
1351+
1352+
# Google OIDC publishers are unique on the tuple of (email, sub), so we
1353+
# check for an already registered one before creating.
1354+
publisher = (
1355+
self.request.db.query(GooglePublisher)
1356+
.filter(
1357+
GooglePublisher.email == form.email.data,
1358+
GooglePublisher.sub == form.sub.data,
1359+
)
1360+
.one_or_none()
1361+
)
1362+
if publisher is None:
1363+
publisher = GooglePublisher(
1364+
email=form.email.data,
1365+
sub=form.sub.data,
1366+
)
1367+
1368+
self.request.db.add(publisher)
1369+
1370+
# Each project has a unique set of OIDC publishers; the same
1371+
# publisher can't be registered to the project more than once.
1372+
if publisher in self.project.oidc_publishers:
1373+
self.request.session.flash(
1374+
self.request._(
1375+
f"{publisher} is already registered with {self.project.name}"
1376+
),
1377+
queue="error",
1378+
)
1379+
return response
1380+
1381+
for user in self.project.users:
1382+
send_trusted_publisher_added_email(
1383+
self.request,
1384+
user,
1385+
project_name=self.project.name,
1386+
publisher=publisher,
1387+
)
1388+
1389+
self.project.oidc_publishers.append(publisher)
1390+
1391+
self.project.record_event(
1392+
tag=EventTag.Project.OIDCPublisherAdded,
1393+
ip_address=self.request.remote_addr,
1394+
request=self.request,
1395+
additional={
1396+
"publisher": publisher.publisher_name,
1397+
"id": str(publisher.id),
1398+
"specifier": str(publisher),
1399+
"url": publisher.publisher_url(),
1400+
"submitted_by": self.request.user.username,
1401+
},
1402+
)
1403+
1404+
self.request.session.flash(
1405+
f"Added {publisher} in {publisher.publisher_url()} to {self.project.name}",
1406+
queue="success",
1407+
)
1408+
1409+
self.metrics.increment(
1410+
"warehouse.oidc.add_publisher.ok", tags=["publisher:GitHub"]
1411+
)
1412+
1413+
return HTTPSeeOther(self.request.path)
1414+
13021415
@view_config(
13031416
request_method="POST",
13041417
request_param=DeletePublisherForm.__params__,

warehouse/oidc/forms/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212

1313
from warehouse.oidc.forms._core import DeletePublisherForm
1414
from warehouse.oidc.forms.github import GitHubPublisherForm, PendingGitHubPublisherForm
15+
from warehouse.oidc.forms.google import GooglePublisherForm, PendingGooglePublisherForm
1516

1617
__all__ = [
1718
"DeletePublisherForm",
1819
"GitHubPublisherForm",
1920
"PendingGitHubPublisherForm",
21+
"GooglePublisherForm",
22+
"PendingGooglePublisherForm",
2023
]

0 commit comments

Comments
 (0)