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

Models, routes and views for creating OIDC publishers #10753

Merged
merged 93 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
48ab12e
warehouse/oidc: rough model skeleton
woodruffw Feb 14, 2022
7db2c2c
warehouse/oidc: fix imports
woodruffw Feb 14, 2022
eab5e70
warehouse/migrations: add migration for OIDC models
woodruffw Feb 14, 2022
09d0966
warehouse/migrations: reformat
woodruffw Feb 14, 2022
24b9eab
warehouse/oidc: add basic verification logic
woodruffw Feb 14, 2022
c56476f
oidc/services: reduce clock skew leeway to 30s
woodruffw Feb 14, 2022
41a1ca0
warehouse/oidc: refactor claim verification
woodruffw Feb 15, 2022
63d16a2
oidc/models: fill in missing properties
woodruffw Feb 15, 2022
50549ec
warehouse/migrations: remove original OIDC migration
woodruffw Feb 15, 2022
f1d7162
warehouse: add OIDC migration, fix association
woodruffw Feb 15, 2022
5479c81
warehouse: reformat
woodruffw Feb 15, 2022
1e0f26c
warehouse: OIDC route/view skeleton work
woodruffw Feb 16, 2022
bf5859b
warehouse: form, view logic for adding OIDC providers
woodruffw Feb 17, 2022
a62a917
manage/views: disable HTTP cache, add TODO
woodruffw Feb 17, 2022
ed1559c
warehouse: move oidc views to "publishing"
woodruffw Feb 18, 2022
971ccf2
warehouse: provider deletion routing
woodruffw Feb 18, 2022
4aad8ee
warehouse: shore up constraints, better error flashes
woodruffw Feb 18, 2022
ac48e82
Merge branch 'main' into tob-oidc-db-models
woodruffw Feb 18, 2022
ba1a39d
Merge branch 'main' into tob-oidc-db-models
woodruffw Feb 25, 2022
100120c
warehouse/migrations: rebase revision
woodruffw Feb 25, 2022
1ca025a
warehouse/templates: update OIDC language
woodruffw Feb 25, 2022
d54dd38
warehouse: OIDC rate limiting groundwork
woodruffw Feb 25, 2022
c700c92
manage/views: clean up OIDC events
woodruffw Feb 25, 2022
d215931
warehouse: use GitHub token for API requests, when available
woodruffw Feb 25, 2022
232ae6f
oidc/forms: special casing for rate limiting
woodruffw Feb 25, 2022
62a795f
warehouse: split user/repo form inputs apart
woodruffw Feb 25, 2022
5ed70bc
warehouse/templates: link to GitHub's OIDC docs
woodruffw Feb 25, 2022
2c9722d
oidc/models: remove actor from checked claims
woodruffw Feb 25, 2022
be82d1b
templates/email: add OIDC email templates
woodruffw Feb 28, 2022
6c00487
warehouse: fix templates, add email sending logic
woodruffw Feb 28, 2022
a6aa4a0
warehouse: add an AdminFlag for OIDC control
woodruffw Feb 28, 2022
e259dc8
oidc/models: use set operators
woodruffw Feb 28, 2022
d0b37d2
oidc/forms: exception driven handling for GitHub API errors
woodruffw Feb 28, 2022
1fc826a
warehouse: OIDC ratelimiting logic
woodruffw Mar 2, 2022
ae1f6bb
Merge branch 'main' into tob-oidc-db-models
woodruffw Mar 2, 2022
f307de7
warehouse/locale: update translations
woodruffw Mar 2, 2022
bd568c8
Merge remote-tracking branch 'upstream/main' into tob-oidc-db-models
woodruffw Mar 7, 2022
5bfba05
Merge branch 'main' into tob-oidc-db-models
woodruffw Mar 7, 2022
67fb78c
warehouse: lintage
woodruffw Mar 7, 2022
d249c6b
templates/manage/settings: remove vestigial HTML
woodruffw Mar 9, 2022
07a7119
warehouse: address feedback
woodruffw Mar 9, 2022
788ddfa
manage/views: more feedback addressing
woodruffw Mar 9, 2022
77c30d9
Update warehouse/manage/views.py
woodruffw Mar 9, 2022
7c6a293
manage/views: fixups
woodruffw Mar 9, 2022
b864d94
warehouse: add "OIDC provider removed" emails
woodruffw Mar 9, 2022
f726919
oidc/forms: use GH org regex in callable validator body
woodruffw Mar 9, 2022
901615a
Merge remote-tracking branch 'upstream/main' into tob-oidc-db-models
woodruffw Mar 9, 2022
63caa2a
warehouse/locale: update translations
woodruffw Mar 9, 2022
69ba7db
tests, warehouse: begin writing unit tests
woodruffw Mar 9, 2022
c72357b
More tests, restructure for testing
woodruffw Mar 9, 2022
45e2721
tests: fill in GitHubProviderForm tests
woodruffw Mar 9, 2022
f6fde8d
tests, warehouse: more tests, adaptations for testing
woodruffw Mar 10, 2022
17b2473
tests: more manage/view tests
woodruffw Mar 10, 2022
6d3130b
tests, warehouse: ratelimit tests, fix bug
woodruffw Mar 10, 2022
8bd4d04
tests: round out ratelimiting
woodruffw Mar 10, 2022
e8f1a8d
tests: more tests
woodruffw Mar 10, 2022
d12c66e
Merge remote-tracking branch 'upstream/main' into tob-oidc-db-models
woodruffw Mar 11, 2022
ce65932
tests, warehouse: OIDC deletion tests
woodruffw Mar 11, 2022
792b306
tests, warehouse: fill in model checks
woodruffw Mar 11, 2022
2215f39
oidc/models: type hints
woodruffw Mar 11, 2022
1a21b4f
warehouse/locale: `make translations`
woodruffw Mar 11, 2022
7bb51bb
Merge branch 'main' into tob-oidc-db-models
woodruffw Mar 14, 2022
cc0c21a
tests, warehouse: site-wide OIDC feature flag
woodruffw Mar 14, 2022
3fa705e
warehouse: `make translations`
woodruffw Mar 14, 2022
c2ca980
treewide: route to 404 when OIDC is disabled
woodruffw Mar 17, 2022
9c459d6
Merge remote-tracking branch 'upstream/main' into tob-oidc-db-models
woodruffw Mar 17, 2022
f6b2cf0
warehouse: `make translations`
woodruffw Mar 17, 2022
8912a40
Update warehouse/templates/manage/publishing.html
woodruffw Mar 21, 2022
a9a9175
Merge branch 'main' into tob-oidc-db-models
woodruffw Mar 21, 2022
7482d78
oidc/{interfaces,services}: simplify API
woodruffw Mar 21, 2022
17eae83
tests: update
woodruffw Mar 22, 2022
c94b2b8
warehouse/migrations: rebase
woodruffw Mar 22, 2022
1d4e12b
tests, warehouse: move ratelimit hit up
woodruffw Mar 22, 2022
41f4426
Merge branch 'main' into tob-oidc-db-models
woodruffw Mar 28, 2022
7e380c9
warehouse: `make translations`
woodruffw Mar 28, 2022
5dafd98
warehouse: plug in more OIDC metrics
woodruffw Mar 29, 2022
0ff3f01
warehouse/oidc: add a `verify_for_helper` iface method
woodruffw Mar 29, 2022
c945ee7
manage/views: add provider names to metrics
woodruffw Mar 29, 2022
32cfd38
oidc/services: add project tag to metrics during JWT verification
woodruffw Mar 29, 2022
63bce66
oidc/services: include provider name in metrics too
woodruffw Mar 29, 2022
b677a8d
tests/unit: plumb metrics through OIDC unit tests
woodruffw Mar 30, 2022
f9813ad
tests/unit: fill in coverage
woodruffw Mar 30, 2022
dd95500
warehouse: `make translations`
woodruffw Mar 30, 2022
8df5ccd
Merge branch 'main' into tob-oidc-db-models
woodruffw Mar 31, 2022
c9705fe
Merge branch 'main' into tob-oidc-db-models
woodruffw Apr 1, 2022
04c7261
tests, warehouse: disable `job_workflow_ref`
woodruffw Apr 1, 2022
de52f9f
Merge branch 'main' into tob-oidc-db-models
woodruffw Apr 4, 2022
4609559
Apply suggestions from code review
woodruffw Apr 5, 2022
52c4e15
tests, warehouse: update tests for changes
woodruffw Apr 5, 2022
73eef39
warehouse, tests: email all users on OIDC changes
woodruffw Apr 5, 2022
4651ce8
warehouse, tests: include publisher info in OIDC emails
woodruffw Apr 5, 2022
e090ad3
warehouse: `make translations`
woodruffw Apr 5, 2022
eeb599d
Merge branch 'main' into tob-oidc-db-models
di Apr 5, 2022
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Add initial OIDC provider models

Revision ID: f345394c444f
Revises: 29a8901a4635
Create Date: 2022-02-15 21:11:41.693791
"""

import sqlalchemy as sa

from alembic import op
from sqlalchemy.dialects import postgresql

revision = "f345394c444f"
down_revision = "29a8901a4635"

# Note: It is VERY important to ensure that a migration does not lock for a
# long period of time and to ensure that each individual migration does
# not break compatibility with the *previous* version of the code base.
# This is because the migrations will be ran automatically as part of the
# deployment process, but while the previous version of the code is still
# up and running. Thus backwards incompatible changes must be broken up
# over multiple migrations inside of multiple pull requests in order to
# phase them in over multiple deploys.


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"oidc_providers",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("discriminator", sa.String(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"github_oidc_providers",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("repository_name", sa.String(), nullable=True),
sa.Column("owner", sa.String(), nullable=True),
sa.Column("owner_id", sa.String(), nullable=True),
sa.Column("workflow_name", sa.String(), nullable=True),
sa.ForeignKeyConstraint(
["id"],
["oidc_providers.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"oidc_provider_project_association",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("oidc_provider_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
["oidc_provider_id"],
["oidc_providers.id"],
),
sa.ForeignKeyConstraint(
["project_id"],
["projects.id"],
),
sa.PrimaryKeyConstraint("id", "oidc_provider_id", "project_id"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("oidc_provider_project_association")
op.drop_table("github_oidc_providers")
op.drop_table("oidc_providers")
# ### end Alembic commands ###
11 changes: 9 additions & 2 deletions warehouse/oidc/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ def get_key(key_id):
"""
pass

def verify(token):
def verify_signature_only(token):
"""
Verify the given JWT.
Verify the given JWT's signature and basic claims, returning
a tuple of (valid, decoded) where `valid` indicates
the validity of the JWT and `decoded` is the decoded JWT,
or `None` if invalid.

This function **does not** verify the token's suitability
for a particular action; subsequent checks on the decoded token's
third party claims must be done to ensure that.
"""
144 changes: 144 additions & 0 deletions warehouse/oidc/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sentry_sdk

from sqlalchemy import Column, ForeignKey, String, orm
from sqlalchemy.dialects.postgresql import UUID

from warehouse import db
from warehouse.packaging.models import Project


class OIDCProviderProjectAssociation(db.Model):
__tablename__ = "oidc_provider_project_association"

oidc_provider_id = Column(
UUID(as_uuid=True),
ForeignKey("oidc_providers.id"),
nullable=False,
primary_key=True,
)
project_id = Column(
UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False, primary_key=True
)
Comment on lines +25 to +36
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: Let me know if there's a better way to do this many-many mapping. This is the pattern I'm familiar with, but there are probably others.



class OIDCProvider(db.Model):
__tablename__ = "oidc_providers"

discriminator = Column(String)
projects = orm.relationship(
Project,
secondary=OIDCProviderProjectAssociation.__table__,
backref="oidc_providers",
)

__mapper_args__ = {"polymorphic_on": discriminator}

# A map of claim names to "check" functions, each of which
# has the signature `check(ground-truth, signed-claim) -> bool`.
__verifiable_claims__ = dict()

# Claims that have already been verified during the JWT signature
# verification phase.
__preverified_claims__ = {
"iss",
"iat",
"nbf",
"exp",
"aud",
}

# Individual providers should explicitly override this set,
# indicating any custom claims that are known to be present but are
# not checked as part of verifying the JWT.
__unchecked_claims__ = set()

def verify_claims(self, signed_claims):
"""
Given a JWT that has been successfully decoded (checked for a valid
signature and basic claims), verify it against the more specific
claims of this provider.
"""

# Defensive programming: treat the absence of any claims to verify
# as a failure rather than trivially valid.
if not self.__verifiable_claims__:
return False

# All claims should be accounted for.
# The presence of an unaccounted claim is not an error, only a warning
# that the JWT payload has changed.
known_claims = self.__verifiable_claims__.keys().union(
self.__preverified_claims__, self.__unchecked_claims__
)
unaccounted_claims = known_claims.difference(signed_claims.keys())
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
if unaccounted_claims:
sentry_sdk.capture_message(
f"JWT for {self.__class__.__name__} has unaccounted claims: {unaccounted_claims}"
)

# Finally, perform the actual claim verification.
for claim_name, check in self.__verifiable_claims__.items():
if not check(self.getattr(claim_name), signed_claims[claim_name]):
return False

return True


class GitHubProvider(OIDCProvider):
__tablename__ = "github_oidc_providers"
__mapper_args__ = {"polymorphic_identity": "GitHubProvider"}

id = Column(UUID(as_uuid=True), ForeignKey(OIDCProvider.id), primary_key=True)
repository_name = Column(String)
owner = Column(String)
owner_id = Column(String)
workflow_name = Column(String)

__verifiable_claims__ = {
"repository": str.__eq__,
"job_workflow_ref": str.startswith,
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
"actor": str.__eq__,
"workflow": str.__eq__,
}

__unchecked_claims__ = {
"jti",
"sub",
"ref",
"sha",
"run_id",
"run_number",
"run_attempt",
"head_ref",
"base_ref",
"event_name",
"ref_type",
}

@property
def repository(self):
return f"{self.owner}/{self.repository_name}"

@property
def job_workflow_ref(self):
return f"{self.repository}/.github/workflows/{self.workflow_name}.yml"

@property
def actor(self):
return self.owner
woodruffw marked this conversation as resolved.
Show resolved Hide resolved

@property
def workflow(self):
return self.workflow_name
49 changes: 45 additions & 4 deletions warehouse/oidc/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@

import json

import jwt
import redis
import requests
import sentry_sdk

from jwt import PyJWK
from zope.interface import implementer

from warehouse.metrics.interfaces import IMetricsService
Expand Down Expand Up @@ -148,10 +148,51 @@ def get_key(self, key_id):
tags=[f"provider:{self.provider}", f"key_id:{key_id}"],
)
return None
return PyJWK(keyset[key_id])
return jwt.PyJWK(keyset[key_id])

def verify(self, token):
return NotImplemented
def _get_key_for_token(self, token):
"""
Return a JWK suitable for verifying the given JWT.

The JWT is not verified at this point, and this step happens
prior to any verification.
"""
unverified_header = jwt.get_unverified_header(token)
return self.get_key(unverified_header["kid"])

def verify_signature_only(self, token):
key = self._get_key_for_token(token)

try:
# NOTE: Many of the keyword arguments here are defaults, but we
# set them explicitly to assert the intended verification behavior.
valid_token = jwt.decode(
token,
key=key,
algorithms=["RS256"],
verify_signature=True,
# "require" only checks for the presence of these claims, not
# their validity. Each has a corresponding "verify_" kwarg
# that enforces their actual validity.
require=["iss", "iat", "nbf", "exp", "aud"],
verify_iss=True,
verify_iat=True,
verify_nbf=True,
verify_exp=True,
verify_aud=True,
issuer=self.issuer_url,
audience="pypi",
leeway=30,
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
)
return True, valid_token
except jwt.PyJWTError:
return False, None
except Exception as e:
# We expect pyjwt to only raise subclasses of PyJWTError, but
# we can't enforce this. Other exceptions indicate an abstraction
# leak, so we log them for upstream reporting.
sentry_sdk.capture_message(f"JWT verify raised generic error: {e}")
return False, None
woodruffw marked this conversation as resolved.
Show resolved Hide resolved


class OIDCProviderServiceFactory:
Expand Down