Skip to content

Commit 3c93c03

Browse files
woodruffwdiewjoachim
authored
Models, routes and views for creating OIDC publishers (#10753)
* warehouse/oidc: rough model skeleton * warehouse/oidc: fix imports * warehouse/migrations: add migration for OIDC models * warehouse/migrations: reformat * warehouse/oidc: add basic verification logic * oidc/services: reduce clock skew leeway to 30s * warehouse/oidc: refactor claim verification * oidc/models: fill in missing properties * warehouse/migrations: remove original OIDC migration Add many-many project-provider association. * warehouse: add OIDC migration, fix association * warehouse: reformat * warehouse: OIDC route/view skeleton work * warehouse: form, view logic for adding OIDC providers * manage/views: disable HTTP cache, add TODO * warehouse: move oidc views to "publishing" ...and make it a sub-page for project management. * warehouse: provider deletion routing * warehouse: shore up constraints, better error flashes * warehouse/migrations: rebase revision * warehouse/templates: update OIDC language Refer to OIDC providers as "OpenID Connect publishers" * warehouse: OIDC rate limiting groundwork * manage/views: clean up OIDC events * warehouse: use GitHub token for API requests, when available * oidc/forms: special casing for rate limiting Record errors with Sentry. * warehouse: split user/repo form inputs apart * warehouse/templates: link to GitHub's OIDC docs * oidc/models: remove actor from checked claims * templates/email: add OIDC email templates * warehouse: fix templates, add email sending logic * warehouse: add an AdminFlag for OIDC control * oidc/models: use set operators * oidc/forms: exception driven handling for GitHub API errors * warehouse: OIDC ratelimiting logic Also some small HTML fixes. * warehouse/locale: update translations * warehouse: lintage * templates/manage/settings: remove vestigial HTML * warehouse: address feedback * Simplify form handling * Validate GitHub usernames against a regex * Fix form error presentation * manage/views: more feedback addressing * Prevent an infoleak in a session flash * Reword a confusing comment * Update warehouse/manage/views.py Co-authored-by: Dustin Ingram <di@users.noreply.github.com> * manage/views: fixups * warehouse: add "OIDC provider removed" emails * oidc/forms: use GH org regex in callable validator body * warehouse/locale: update translations * tests, warehouse: begin writing unit tests * More tests, restructure for testing * tests: fill in GitHubProviderForm tests * tests, warehouse: more tests, adaptations for testing * tests: more manage/view tests * tests, warehouse: ratelimit tests, fix bug * tests: round out ratelimiting * tests: more tests * tests, warehouse: OIDC deletion tests Also, gets some coverage for free by reusing a helper. * tests, warehouse: fill in model checks Accommodations for testing. * oidc/models: type hints * warehouse/locale: `make translations` * tests, warehouse: site-wide OIDC feature flag * warehouse: `make translations` * treewide: route to 404 when OIDC is disabled Enable OIDC by default for development environments; update tests. * warehouse: `make translations` * Update warehouse/templates/manage/publishing.html Co-authored-by: Joachim Jablon <ewjoachim@gmail.com> * oidc/{interfaces,services}: simplify API * tests: update * warehouse/migrations: rebase * tests, warehouse: move ratelimit hit up * warehouse: `make translations` * warehouse: plug in more OIDC metrics Adds additional metrics on: * Publisher configuration (attempt + ok) * Publisher removal (attempt + ok) * JWT signature verification (attempt + ok) * warehouse/oidc: add a `verify_for_helper` iface method This encapsulates the entire JWT verification process. It isn't hooked up to anything yet, but just to get something down. * manage/views: add provider names to metrics * oidc/services: add project tag to metrics during JWT verification * oidc/services: include provider name in metrics too * tests/unit: plumb metrics through OIDC unit tests * tests/unit: fill in coverage * warehouse: `make translations` * tests, warehouse: disable `job_workflow_ref` For now. * Apply suggestions from code review Co-authored-by: Dustin Ingram <di@users.noreply.github.com> * tests, warehouse: update tests for changes Also use `workflow_filename` consistently. * warehouse, tests: email all users on OIDC changes Instead of just owners. * warehouse, tests: include publisher info in OIDC emails * warehouse: `make translations` Co-authored-by: Dustin Ingram <di@users.noreply.github.com> Co-authored-by: Joachim Jablon <ewjoachim@gmail.com>
1 parent 5112d7d commit 3c93c03

32 files changed

+2969
-52
lines changed

dev/environment

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ GITHUB_TOKEN_SCANNING_META_API_URL="http://notgithub:8000/meta/public_keys/token
4949
TWOFACTORREQUIREMENT_ENABLED=true
5050
TWOFACTORMANDATE_AVAILABLE=true
5151
TWOFACTORMANDATE_ENABLED=true
52+
OIDC_ENABLED=true

tests/unit/email/test_init.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3575,3 +3575,96 @@ def test_recovery_code_emails(
35753575
},
35763576
)
35773577
]
3578+
3579+
3580+
class TestOIDCProviderEmails:
3581+
@pytest.mark.parametrize(
3582+
"fn, template_name",
3583+
[
3584+
(email.send_oidc_provider_added_email, "oidc-provider-added"),
3585+
(email.send_oidc_provider_removed_email, "oidc-provider-removed"),
3586+
],
3587+
)
3588+
def test_oidc_provider_emails(
3589+
self, pyramid_request, pyramid_config, monkeypatch, fn, template_name
3590+
):
3591+
stub_user = pretend.stub(
3592+
id="id",
3593+
username="username",
3594+
name="",
3595+
email="email@example.com",
3596+
primary_email=pretend.stub(email="email@example.com", verified=True),
3597+
)
3598+
subject_renderer = pyramid_config.testing_add_renderer(
3599+
f"email/{ template_name }/subject.txt"
3600+
)
3601+
subject_renderer.string_response = "Email Subject"
3602+
body_renderer = pyramid_config.testing_add_renderer(
3603+
f"email/{ template_name }/body.txt"
3604+
)
3605+
body_renderer.string_response = "Email Body"
3606+
html_renderer = pyramid_config.testing_add_renderer(
3607+
f"email/{ template_name }/body.html"
3608+
)
3609+
html_renderer.string_response = "Email HTML Body"
3610+
3611+
send_email = pretend.stub(
3612+
delay=pretend.call_recorder(lambda *args, **kwargs: None)
3613+
)
3614+
pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
3615+
monkeypatch.setattr(email, "send_email", send_email)
3616+
3617+
pyramid_request.db = pretend.stub(
3618+
query=lambda a: pretend.stub(
3619+
filter=lambda *a: pretend.stub(
3620+
one=lambda: pretend.stub(user_id=stub_user.id)
3621+
)
3622+
),
3623+
)
3624+
pyramid_request.user = stub_user
3625+
pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
3626+
3627+
project_name = "test_project"
3628+
fakeprovider = pretend.stub(provider_name="fakeprovider")
3629+
# NOTE: Can't set __str__ using pretend.stub()
3630+
monkeypatch.setattr(
3631+
fakeprovider.__class__, "__str__", lambda s: "fakespecifier"
3632+
)
3633+
3634+
result = fn(
3635+
pyramid_request, stub_user, project_name=project_name, provider=fakeprovider
3636+
)
3637+
3638+
assert result == {
3639+
"username": stub_user.username,
3640+
"project_name": project_name,
3641+
"provider_name": "fakeprovider",
3642+
"provider_spec": "fakespecifier",
3643+
}
3644+
subject_renderer.assert_()
3645+
body_renderer.assert_(username=stub_user.username, project_name=project_name)
3646+
html_renderer.assert_(username=stub_user.username, project_name=project_name)
3647+
assert pyramid_request.task.calls == [pretend.call(send_email)]
3648+
assert send_email.delay.calls == [
3649+
pretend.call(
3650+
f"{stub_user.username} <{stub_user.email}>",
3651+
{
3652+
"subject": "Email Subject",
3653+
"body_text": "Email Body",
3654+
"body_html": (
3655+
"<html>\n<head></head>\n"
3656+
"<body><p>Email HTML Body</p></body>\n</html>\n"
3657+
),
3658+
},
3659+
{
3660+
"tag": "account:email:sent",
3661+
"user_id": stub_user.id,
3662+
"additional": {
3663+
"from_": "noreply@example.com",
3664+
"to": stub_user.email,
3665+
"subject": "Email Subject",
3666+
"redact_ip": False,
3667+
},
3668+
},
3669+
)
3670+
]

tests/unit/manage/test_init.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,37 @@ def view(context, request):
9494
assert request.session.needs_reauthentication.calls == needs_reauth_calls
9595

9696

97-
def test_includeme():
97+
def test_includeme(monkeypatch):
98+
settings = {
99+
"warehouse.manage.oidc.user_registration_ratelimit_string": "10 per day",
100+
"warehouse.manage.oidc.ip_registration_ratelimit_string": "100 per day",
101+
}
102+
98103
config = pretend.stub(
99104
add_view_deriver=pretend.call_recorder(lambda f, over, under: None),
105+
register_service_factory=pretend.call_recorder(lambda s, i, **kw: None),
106+
registry=pretend.stub(
107+
settings=pretend.stub(get=pretend.call_recorder(lambda k: settings.get(k)))
108+
),
100109
)
101110

111+
rate_limit_class = pretend.call_recorder(lambda s: s)
112+
rate_limit_iface = pretend.stub()
113+
monkeypatch.setattr(manage, "RateLimit", rate_limit_class)
114+
monkeypatch.setattr(manage, "IRateLimiter", rate_limit_iface)
115+
102116
manage.includeme(config)
103117

104118
assert config.add_view_deriver.calls == [
105119
pretend.call(manage.reauth_view, over="rendered_view", under="decorated_view")
106120
]
121+
assert config.register_service_factory.calls == [
122+
pretend.call(
123+
"10 per day", rate_limit_iface, name="user_oidc.provider.register"
124+
),
125+
pretend.call("100 per day", rate_limit_iface, name="ip_oidc.provider.register"),
126+
]
127+
assert config.registry.settings.get.calls == [
128+
pretend.call("warehouse.manage.oidc.user_registration_ratelimit_string"),
129+
pretend.call("warehouse.manage.oidc.ip_registration_ratelimit_string"),
130+
]

0 commit comments

Comments
 (0)