Skip to content

Commit 005f79c

Browse files
committed
wip
1 parent 2c4649b commit 005f79c

File tree

12 files changed

+952
-12
lines changed

12 files changed

+952
-12
lines changed

warehouse/accounts/views.py

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,14 @@
7777
from warehouse.events.tags import EventTag
7878
from warehouse.metrics.interfaces import IMetricsService
7979
from warehouse.oidc.forms import DeletePublisherForm
80+
from warehouse.oidc.forms.buildkite import PendingBuildkitePublisherForm
8081
from warehouse.oidc.forms.github import PendingGitHubPublisherForm
8182
from warehouse.oidc.interfaces import TooManyOIDCRegistrations
82-
from warehouse.oidc.models import PendingGitHubPublisher, PendingOIDCPublisher
83+
from warehouse.oidc.models import (
84+
PendingBuildkitePublisher,
85+
PendingGitHubPublisher,
86+
PendingOIDCPublisher,
87+
)
8388
from warehouse.organizations.interfaces import IOrganizationService
8489
from warehouse.organizations.models import OrganizationRole, OrganizationRoleType
8590
from warehouse.packaging.models import (
@@ -1488,6 +1493,13 @@ def _check_ratelimits(self):
14881493
)
14891494
)
14901495

1496+
@property
1497+
def pending_buildkite_publisher_form(self):
1498+
return PendingBuildkitePublisherForm(
1499+
self.request.POST,
1500+
project_factory=self.project_factory
1501+
)
1502+
14911503
@property
14921504
def pending_github_publisher_form(self):
14931505
return PendingGitHubPublisherForm(
@@ -1499,6 +1511,8 @@ def pending_github_publisher_form(self):
14991511
@property
15001512
def default_response(self):
15011513
return {
1514+
"default_publisher_name": "github",
1515+
"pending_buildkite_publisher_form": self.pending_buildkite_publisher_form,
15021516
"pending_github_publisher_form": self.pending_github_publisher_form,
15031517
}
15041518

@@ -1516,11 +1530,147 @@ def manage_publishing(self):
15161530

15171531
return self.default_response
15181532

1533+
@view_config(
1534+
route_name="manage.account.publishing.buildkite",
1535+
request_method="POST",
1536+
request_param=PendingBuildkitePublisherForm.__params__,
1537+
)
1538+
def add_pending_buildkite_oidc_publisher(self):
1539+
response = self.default_response
1540+
response["default_publisher_name"] = "buildkite"
1541+
1542+
if self.request.flags.disallow_oidc(AdminFlagValue.DISALLOW_BUILDKITE_OIDC):
1543+
self.request.session.flash(
1544+
self.request._(
1545+
"Buildkite-based trusted publishing is temporarily disabled. "
1546+
"See https://pypi.org/help#admin-intervention for details."
1547+
),
1548+
queue="error",
1549+
)
1550+
return response
1551+
1552+
self.metrics.increment(
1553+
"warehouse.oidc.add_pending_publisher.attempt", tags=["publisher:Buildkite"]
1554+
)
1555+
1556+
if not self.request.user.has_primary_verified_email:
1557+
self.request.session.flash(
1558+
self.request._(
1559+
"You must have a verified email in order to register a "
1560+
"pending trusted publisher. "
1561+
"See https://pypi.org/help#openid-connect for details."
1562+
),
1563+
queue="error",
1564+
)
1565+
return response
1566+
1567+
# Separately from having permission to register pending OIDC publishers,
1568+
# we limit users to no more than 3 pending publishers at once.
1569+
if len(self.request.user.pending_oidc_publishers) >= 3:
1570+
self.request.session.flash(
1571+
self.request._(
1572+
"You can't register more than 3 pending trusted "
1573+
"publishers at once."
1574+
),
1575+
queue="error",
1576+
)
1577+
return response
1578+
1579+
try:
1580+
self._check_ratelimits()
1581+
except TooManyOIDCRegistrations as exc:
1582+
self.metrics.increment(
1583+
"warehouse.oidc.add_pending_publisher.ratelimited",
1584+
tags=["publisher:Buildkite"],
1585+
)
1586+
return HTTPTooManyRequests(
1587+
self.request._(
1588+
"There have been too many attempted trusted publisher "
1589+
"registrations. Try again later."
1590+
),
1591+
retry_after=exc.resets_in.total_seconds(),
1592+
)
1593+
1594+
self._hit_ratelimits()
1595+
1596+
response = response
1597+
form = response["pending_buildkite_publisher_form"]
1598+
1599+
if not form.validate():
1600+
self.request.session.flash(
1601+
self.request._("The trusted publisher could not be registered"),
1602+
queue="error",
1603+
)
1604+
return response
1605+
1606+
publisher_already_exists = (
1607+
self.request.db.query(PendingBuildkitePublisher)
1608+
.filter_by(
1609+
organization_slug=form.organization_slug.data,
1610+
pipeline_slug=form.pipeline_slug.data,
1611+
)
1612+
.first()
1613+
is not None
1614+
)
1615+
1616+
if publisher_already_exists:
1617+
self.request.session.flash(
1618+
self.request._(
1619+
"This trusted publisher has already been registered. "
1620+
"Please contact PyPI's admins if this wasn't intentional."
1621+
),
1622+
queue="error",
1623+
)
1624+
return response
1625+
1626+
pending_publisher = PendingBuildkitePublisher(
1627+
project_name=form.project_name.data,
1628+
added_by=self.request.user,
1629+
organization_slug=form.organization_slug.data,
1630+
pipeline_slug=form.pipeline_slug.data,
1631+
build_branch=form.build_branch.data,
1632+
build_tag=form.build_tag.data,
1633+
step_key=form.step_key.data,
1634+
)
1635+
1636+
self.request.db.add(pending_publisher)
1637+
self.request.db.flush() # To get the new ID
1638+
1639+
self.request.user.record_event(
1640+
tag=EventTag.Account.PendingOIDCPublisherAdded,
1641+
request=self.request,
1642+
additional={
1643+
"project": pending_publisher.project_name,
1644+
"publisher": pending_publisher.publisher_name,
1645+
"id": str(pending_publisher.id),
1646+
"specifier": str(pending_publisher),
1647+
"url": pending_publisher.publisher_url(),
1648+
"submitted_by": self.request.user.username,
1649+
},
1650+
)
1651+
1652+
self.request.session.flash(
1653+
self.request._(
1654+
"Registered a new pending publisher to create "
1655+
f"the project '{pending_publisher.project_name}'."
1656+
),
1657+
queue="success",
1658+
)
1659+
1660+
self.metrics.increment(
1661+
"warehouse.oidc.add_pending_publisher.ok", tags=["publisher:Buildkite"]
1662+
)
1663+
1664+
return HTTPSeeOther(self.request.route_path("manage.account.publishing"))
1665+
15191666
@view_config(
15201667
request_method="POST",
15211668
request_param=PendingGitHubPublisherForm.__params__,
15221669
)
15231670
def add_pending_github_oidc_publisher(self):
1671+
response = self.default_response
1672+
response["default_publisher_name"] = "github"
1673+
15241674
if self.request.flags.disallow_oidc(AdminFlagValue.DISALLOW_GITHUB_OIDC):
15251675
self.request.session.flash(
15261676
self.request._(
@@ -1529,7 +1679,7 @@ def add_pending_github_oidc_publisher(self):
15291679
),
15301680
queue="error",
15311681
)
1532-
return self.default_response
1682+
return response
15331683

15341684
self.metrics.increment(
15351685
"warehouse.oidc.add_pending_publisher.attempt", tags=["publisher:GitHub"]

warehouse/admin/flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class AdminFlagValue(enum.Enum):
2525
DISALLOW_NEW_UPLOAD = "disallow-new-upload"
2626
DISALLOW_NEW_USER_REGISTRATION = "disallow-new-user-registration"
2727
DISALLOW_OIDC = "disallow-oidc"
28+
DISALLOW_BUILDKITE_OIDC = "disallow-buildkite-oidc"
2829
DISALLOW_GITHUB_OIDC = "disallow-github-oidc"
2930
DISALLOW_GOOGLE_OIDC = "disallow-google-oidc"
3031
READ_ONLY = "read-only"

warehouse/manage/views/__init__.py

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,14 @@
101101
)
102102
from warehouse.metrics.interfaces import IMetricsService
103103
from warehouse.oidc.forms import DeletePublisherForm
104+
from warehouse.oidc.forms.buildkite import BuildkitePublisherForm
104105
from warehouse.oidc.forms.github import GitHubPublisherForm
105106
from warehouse.oidc.interfaces import TooManyOIDCRegistrations
106-
from warehouse.oidc.models import GitHubPublisher, OIDCPublisher
107+
from warehouse.oidc.models import (
108+
BuildkitePublisher,
109+
GitHubPublisher,
110+
OIDCPublisher,
111+
)
107112
from warehouse.organizations.interfaces import IOrganizationService
108113
from warehouse.organizations.models import (
109114
OrganizationProject,
@@ -1216,6 +1221,12 @@ def _check_ratelimits(self):
12161221
)
12171222
)
12181223

1224+
@property
1225+
def buildkite_publisher_form(self):
1226+
return BuildkitePublisherForm(
1227+
self.request.POST,
1228+
)
1229+
12191230
@property
12201231
def github_publisher_form(self):
12211232
return GitHubPublisherForm(
@@ -1227,6 +1238,8 @@ def github_publisher_form(self):
12271238
def default_response(self):
12281239
return {
12291240
"project": self.project,
1241+
"default_publisher_name": "github",
1242+
"buildkite_publisher_form": self.buildkite_publisher_form,
12301243
"github_publisher_form": self.github_publisher_form,
12311244
}
12321245

@@ -1243,11 +1256,128 @@ def manage_project_oidc_publishers(self):
12431256

12441257
return self.default_response
12451258

1259+
@view_config(
1260+
route_name="manage.project.settings.publishing.buildkite",
1261+
request_method="POST",
1262+
request_param=BuildkitePublisherForm.__params__,
1263+
)
1264+
def add_buildkite_oidc_publisher(self):
1265+
response = self.default_response
1266+
response["default_publisher_name"] = "buildkite"
1267+
1268+
if self.request.flags.disallow_oidc(AdminFlagValue.DISALLOW_BUILDKITE_OIDC):
1269+
self.request.session.flash(
1270+
self.request._(
1271+
"Buildkite-based trusted publishing is temporarily disabled. "
1272+
"See https://pypi.org/help#admin-intervention for details."
1273+
),
1274+
queue="error",
1275+
)
1276+
return response
1277+
1278+
self.metrics.increment(
1279+
"warehouse.oidc.add_publisher.attempt", tags=["publisher:Buildkite"]
1280+
)
1281+
1282+
try:
1283+
self._check_ratelimits()
1284+
except TooManyOIDCRegistrations as exc:
1285+
self.metrics.increment(
1286+
"warehouse.oidc.add_publisher.ratelimited", tags=["publisher:Buildkite"]
1287+
)
1288+
return HTTPTooManyRequests(
1289+
self.request._(
1290+
"There have been too many attempted trusted publisher "
1291+
"registrations. Try again later."
1292+
),
1293+
retry_after=exc.resets_in.total_seconds(),
1294+
)
1295+
1296+
self._hit_ratelimits()
1297+
1298+
form = response["buildkite_publisher_form"]
1299+
1300+
if not form.validate():
1301+
self.request.session.flash(
1302+
self.request._("The trusted publisher could not be registered"),
1303+
queue="error",
1304+
)
1305+
return response
1306+
1307+
# Buildkite OIDC publishers are unique on the tuple of
1308+
# (repository_name, repository_owner, workflow_filename, environment),
1309+
# so we check for an already registered one before creating.
1310+
publisher = (
1311+
self.request.db.query(BuildkitePublisher)
1312+
.filter(
1313+
BuildkitePublisher.organization_slug == form.organization_slug.data,
1314+
BuildkitePublisher.pipeline_slug == form.pipeline_slug.data,
1315+
)
1316+
.one_or_none()
1317+
)
1318+
if publisher is None:
1319+
publisher = BuildkitePublisher(
1320+
organization_slug=form.organization_slug.data,
1321+
pipeline_slug=form.pipeline_slug.data,
1322+
build_branch=form.build_branch.data,
1323+
build_tag=form.build_tag.data,
1324+
step_key=form.step_key.data,
1325+
)
1326+
1327+
self.request.db.add(publisher)
1328+
1329+
# Each project has a unique set of OIDC publishers; the same
1330+
# publisher can't be registered to the project more than once.
1331+
if publisher in self.project.oidc_publishers:
1332+
self.request.session.flash(
1333+
self.request._(
1334+
f"{publisher} is already registered with {self.project.name}"
1335+
),
1336+
queue="error",
1337+
)
1338+
return response
1339+
1340+
for user in self.project.users:
1341+
send_trusted_publisher_added_email(
1342+
self.request,
1343+
user,
1344+
project_name=self.project.name,
1345+
publisher=publisher,
1346+
)
1347+
1348+
self.project.oidc_publishers.append(publisher)
1349+
1350+
self.project.record_event(
1351+
tag=EventTag.Project.OIDCPublisherAdded,
1352+
request=self.request,
1353+
additional={
1354+
"publisher": publisher.publisher_name,
1355+
"id": str(publisher.id),
1356+
"specifier": str(publisher),
1357+
"url": publisher.publisher_url(),
1358+
"submitted_by": self.request.user.username,
1359+
},
1360+
)
1361+
1362+
self.request.session.flash(
1363+
f"Added {publisher} in {publisher.publisher_url()} to {self.project.name}",
1364+
queue="success",
1365+
)
1366+
1367+
self.metrics.increment(
1368+
"warehouse.oidc.add_publisher.ok", tags=["publisher:Buildkite"]
1369+
)
1370+
1371+
return HTTPSeeOther(self.request.path)
1372+
12461373
@view_config(
12471374
request_method="POST",
12481375
request_param=GitHubPublisherForm.__params__,
12491376
)
12501377
def add_github_oidc_publisher(self):
1378+
response = self.default_response
1379+
response["default_publisher_name"] = "github"
1380+
12511381
if self.request.flags.disallow_oidc(AdminFlagValue.DISALLOW_GITHUB_OIDC):
12521382
self.request.session.flash(
12531383
self.request._(
@@ -1256,7 +1386,7 @@ def add_github_oidc_publisher(self):
12561386
),
12571387
queue="error",
12581388
)
1259-
return self.default_response
1389+
return response
12601390

12611391
self.metrics.increment(
12621392
"warehouse.oidc.add_publisher.attempt", tags=["publisher:GitHub"]
@@ -1278,7 +1408,7 @@ def add_github_oidc_publisher(self):
12781408

12791409
self._hit_ratelimits()
12801410

1281-
response = self.default_response
1411+
response = response
12821412
form = response["github_publisher_form"]
12831413

12841414
if not form.validate():

0 commit comments

Comments
 (0)