Skip to content

Commit

Permalink
Add a Google trusted publisher
Browse files Browse the repository at this point in the history
  • Loading branch information
di committed Jan 5, 2024
1 parent b98d4f3 commit 09812df
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 8 deletions.
40 changes: 40 additions & 0 deletions tests/unit/accounts/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3340,11 +3340,19 @@ def test_manage_publishing(self, metrics, monkeypatch):
monkeypatch.setattr(
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
)
pending_google_publisher_form_obj = pretend.stub()
pending_google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: pending_google_publisher_form_obj
)
monkeypatch.setattr(
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
)

view = views.ManageAccountPublishingViews(request)

assert view.manage_publishing() == {
"pending_github_publisher_form": pending_github_publisher_form_obj,
"pending_google_publisher_form": pending_google_publisher_form_obj,
}

assert request.flags.disallow_oidc.calls == [pretend.call()]
Expand Down Expand Up @@ -3382,11 +3390,19 @@ def test_manage_publishing_admin_disabled(self, monkeypatch, pyramid_request):
monkeypatch.setattr(
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
)
pending_google_publisher_form_obj = pretend.stub()
pending_google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: pending_google_publisher_form_obj
)
monkeypatch.setattr(
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
)

view = views.ManageAccountPublishingViews(pyramid_request)

assert view.manage_publishing() == {
"pending_github_publisher_form": pending_github_publisher_form_obj,
"pending_google_publisher_form": pending_google_publisher_form_obj,
}

assert pyramid_request.flags.disallow_oidc.calls == [pretend.call()]
Expand Down Expand Up @@ -3434,11 +3450,19 @@ def test_add_pending_github_oidc_publisher_admin_disabled(
monkeypatch.setattr(
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
)
pending_google_publisher_form_obj = pretend.stub()
pending_google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: pending_google_publisher_form_obj
)
monkeypatch.setattr(
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
)

view = views.ManageAccountPublishingViews(pyramid_request)

assert view.add_pending_github_oidc_publisher() == {
"pending_github_publisher_form": pending_github_publisher_form_obj,
"pending_google_publisher_form": pending_google_publisher_form_obj,
}

assert pyramid_request.flags.disallow_oidc.calls == [
Expand Down Expand Up @@ -3490,11 +3514,19 @@ def test_add_pending_github_oidc_publisher_user_cannot_register(
monkeypatch.setattr(
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
)
pending_google_publisher_form_obj = pretend.stub()
pending_google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: pending_google_publisher_form_obj
)
monkeypatch.setattr(
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
)

view = views.ManageAccountPublishingViews(pyramid_request)

assert view.add_pending_github_oidc_publisher() == {
"pending_github_publisher_form": pending_github_publisher_form_obj,
"pending_google_publisher_form": pending_google_publisher_form_obj,
}

assert pyramid_request.flags.disallow_oidc.calls == [
Expand Down Expand Up @@ -3889,11 +3921,19 @@ def test_delete_pending_oidc_publisher_admin_disabled(
monkeypatch.setattr(
views, "PendingGitHubPublisherForm", pending_github_publisher_form_cls
)
pending_google_publisher_form_obj = pretend.stub()
pending_google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: pending_google_publisher_form_obj
)
monkeypatch.setattr(
views, "PendingGooglePublisherForm", pending_google_publisher_form_cls
)

view = views.ManageAccountPublishingViews(pyramid_request)

assert view.delete_pending_oidc_publisher() == {
"pending_github_publisher_form": pending_github_publisher_form_obj,
"pending_google_publisher_form": pending_google_publisher_form_obj,
}

assert pyramid_request.flags.disallow_oidc.calls == [pretend.call()]
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5837,6 +5837,7 @@ def test_manage_project_oidc_publishers(self, monkeypatch):
assert view.manage_project_oidc_publishers() == {
"project": project,
"github_publisher_form": view.github_publisher_form,
"google_publisher_form": view.google_publisher_form,
}

assert request.flags.disallow_oidc.calls == [pretend.call()]
Expand Down Expand Up @@ -5865,6 +5866,7 @@ def test_manage_project_oidc_publishers_admin_disabled(
assert view.manage_project_oidc_publishers() == {
"project": project,
"github_publisher_form": view.github_publisher_form,
"google_publisher_form": view.google_publisher_form,
}

assert pyramid_request.flags.disallow_oidc.calls == [pretend.call()]
Expand Down Expand Up @@ -5935,6 +5937,11 @@ def test_add_github_oidc_publisher_preexisting(self, metrics, monkeypatch):
lambda *a, **kw: github_publisher_form_obj
)
monkeypatch.setattr(views, "GitHubPublisherForm", github_publisher_form_cls)
google_publisher_form_obj = pretend.stub()
google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: google_publisher_form_obj
)
monkeypatch.setattr(views, "GooglePublisherForm", google_publisher_form_cls)

view = views.ManageOIDCPublisherViews(project, request)
monkeypatch.setattr(
Expand Down Expand Up @@ -6025,6 +6032,11 @@ def test_add_github_oidc_publisher_created(self, metrics, monkeypatch):
lambda *a, **kw: github_publisher_form_obj
)
monkeypatch.setattr(views, "GitHubPublisherForm", github_publisher_form_cls)
google_publisher_form_obj = pretend.stub()
google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: google_publisher_form_obj
)
monkeypatch.setattr(views, "GooglePublisherForm", google_publisher_form_cls)
monkeypatch.setattr(
views,
"send_trusted_publisher_added_email",
Expand Down Expand Up @@ -6129,6 +6141,12 @@ def test_add_github_oidc_publisher_already_registered_with_project(
"_lookup_owner",
lambda *a: {"login": "some-owner", "id": "some-owner-id"},
)
google_publisher_form_obj = pretend.stub()
google_publisher_form_cls = pretend.call_recorder(
lambda *a, **kw: google_publisher_form_obj
)
monkeypatch.setattr(views, "GooglePublisherForm", google_publisher_form_cls)

monkeypatch.setattr(
view, "_hit_ratelimits", pretend.call_recorder(lambda: None)
)
Expand All @@ -6139,6 +6157,7 @@ def test_add_github_oidc_publisher_already_registered_with_project(
assert view.add_github_oidc_publisher() == {
"project": project,
"github_publisher_form": view.github_publisher_form,
"google_publisher_form": view.google_publisher_form,
}
assert view.metrics.increment.calls == [
pretend.call(
Expand Down
41 changes: 38 additions & 3 deletions warehouse/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,17 @@
)
from warehouse.events.tags import EventTag
from warehouse.metrics.interfaces import IMetricsService
from warehouse.oidc.forms import DeletePublisherForm
from warehouse.oidc.forms.github import PendingGitHubPublisherForm
from warehouse.oidc.forms import (
DeletePublisherForm,
PendingGitHubPublisherForm,
PendingGooglePublisherForm,
)
from warehouse.oidc.interfaces import TooManyOIDCRegistrations
from warehouse.oidc.models import PendingGitHubPublisher, PendingOIDCPublisher
from warehouse.oidc.models import (
PendingGitHubPublisher,
PendingGooglePublisher,
PendingOIDCPublisher,
)
from warehouse.organizations.interfaces import IOrganizationService
from warehouse.organizations.models import OrganizationRole, OrganizationRoleType
from warehouse.packaging.models import (
Expand Down Expand Up @@ -1467,6 +1474,10 @@ def __init__(self, request):
api_token=self.request.registry.settings.get("github.token"),
project_factory=self.project_factory,
)
self.pending_google_publisher_form = PendingGooglePublisherForm(
self.request.POST,
project_factory=self.project_factory,
)

@property
def _ratelimiters(self):
Expand Down Expand Up @@ -1502,6 +1513,7 @@ def _check_ratelimits(self):
def default_response(self):
return {
"pending_github_publisher_form": self.pending_github_publisher_form,
"pending_google_publisher_form": self.pending_google_publisher_form,
}

@view_config(request_method="GET")
Expand Down Expand Up @@ -1640,6 +1652,29 @@ def _add_pending_oidc_publisher(

return HTTPSeeOther(self.request.path)

@view_config(
request_method="POST",
request_param=PendingGooglePublisherForm.__params__,
)
def add_pending_google_oidc_publisher(self):
form = self.default_response["pending_google_publisher_form"]
return self._add_pending_oidc_publisher(
publisher_name="Google",
publisher_class=PendingGooglePublisher,
admin_flag=AdminFlagValue.DISALLOW_GOOGLE_OIDC,
form=form,
make_pending_publisher=lambda request, form: PendingGooglePublisher(
project_name=form.project_name.data,
added_by=request.user,
email=form.email.data,
sub=form.sub.data,
),
make_existence_filters=lambda form: dict(
email=form.email.data,
sub=form.sub.data,
),
)

@view_config(
request_method="POST",
request_param=PendingGitHubPublisherForm.__params__,
Expand Down
119 changes: 116 additions & 3 deletions warehouse/manage/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,13 @@
user_projects,
)
from warehouse.metrics.interfaces import IMetricsService
from warehouse.oidc.forms import DeletePublisherForm
from warehouse.oidc.forms.github import GitHubPublisherForm
from warehouse.oidc.forms import (
DeletePublisherForm,
GitHubPublisherForm,
GooglePublisherForm,
)
from warehouse.oidc.interfaces import TooManyOIDCRegistrations
from warehouse.oidc.models import GitHubPublisher, OIDCPublisher
from warehouse.oidc.models import GitHubPublisher, GooglePublisher, OIDCPublisher
from warehouse.organizations.interfaces import IOrganizationService
from warehouse.organizations.models import (
OrganizationProject,
Expand Down Expand Up @@ -1135,6 +1138,7 @@ def __init__(self, project, request):
self.request.POST,
api_token=self.request.registry.settings.get("github.token"),
)
self.google_publisher_form = GooglePublisherForm(self.request.POST)

@property
def _ratelimiters(self):
Expand Down Expand Up @@ -1171,6 +1175,7 @@ def default_response(self):
return {
"project": self.project,
"github_publisher_form": self.github_publisher_form,
"google_publisher_form": self.google_publisher_form,
}

@view_config(request_method="GET")
Expand Down Expand Up @@ -1299,6 +1304,114 @@ def add_github_oidc_publisher(self):

return HTTPSeeOther(self.request.path)

@view_config(
request_method="POST",
request_param=GooglePublisherForm.__params__,
)
def add_google_oidc_publisher(self):
if self.request.flags.disallow_oidc(AdminFlagValue.DISALLOW_GOOGLE_OIDC):
self.request.session.flash(
self.request._(
"Google-based trusted publishing is temporarily disabled. "
"See https://pypi.org/help#admin-intervention for details."
),
queue="error",
)
return self.default_response

self.metrics.increment(
"warehouse.oidc.add_publisher.attempt", tags=["publisher:Google"]
)

try:
self._check_ratelimits()
except TooManyOIDCRegistrations as exc:
self.metrics.increment(
"warehouse.oidc.add_publisher.ratelimited", tags=["publisher:Google"]
)
return HTTPTooManyRequests(
self.request._(
"There have been too many attempted trusted publisher "
"registrations. Try again later."
),
retry_after=exc.resets_in.total_seconds(),
)

self._hit_ratelimits()

response = self.default_response
form = response["google_publisher_form"]

if not form.validate():
self.request.session.flash(
self.request._("The trusted publisher could not be registered"),
queue="error",
)
return response

# Google OIDC publishers are unique on the tuple of (email, sub), so we
# check for an already registered one before creating.
publisher = (
self.request.db.query(GooglePublisher)
.filter(
GooglePublisher.email == form.email.data,
GooglePublisher.sub == form.sub.data,
)
.one_or_none()
)
if publisher is None:
publisher = GooglePublisher(
email=form.email.data,
sub=form.sub.data,
)

self.request.db.add(publisher)

# Each project has a unique set of OIDC publishers; the same
# publisher can't be registered to the project more than once.
if publisher in self.project.oidc_publishers:
self.request.session.flash(
self.request._(
f"{publisher} is already registered with {self.project.name}"
),
queue="error",
)
return response

for user in self.project.users:
send_trusted_publisher_added_email(
self.request,
user,
project_name=self.project.name,
publisher=publisher,
)

self.project.oidc_publishers.append(publisher)

self.project.record_event(
tag=EventTag.Project.OIDCPublisherAdded,
ip_address=self.request.remote_addr,
request=self.request,
additional={
"publisher": publisher.publisher_name,
"id": str(publisher.id),
"specifier": str(publisher),
"url": publisher.publisher_url(),
"submitted_by": self.request.user.username,
},
)

self.request.session.flash(
f"Added {publisher} in {publisher.publisher_url()} to {self.project.name}",
queue="success",
)

self.metrics.increment(
"warehouse.oidc.add_publisher.ok", tags=["publisher:GitHub"]
)

return HTTPSeeOther(self.request.path)

@view_config(
request_method="POST",
request_param=DeletePublisherForm.__params__,
Expand Down
3 changes: 3 additions & 0 deletions warehouse/oidc/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@

from warehouse.oidc.forms._core import DeletePublisherForm
from warehouse.oidc.forms.github import GitHubPublisherForm, PendingGitHubPublisherForm
from warehouse.oidc.forms.google import GooglePublisherForm, PendingGooglePublisherForm

__all__ = [
"DeletePublisherForm",
"GitHubPublisherForm",
"PendingGitHubPublisherForm",
"GooglePublisherForm",
"PendingGooglePublisherForm",
]
Loading

0 comments on commit 09812df

Please sign in to comment.