Skip to content

Commit 609871e

Browse files
facutuescawoodruffwdi
authored
Combine User and UserTokenContext for user-backed identities in requests (#15757)
* Combine User and UserTokenContext for user-backed identities We have three types that a request.identity can be: - `User`: when the identity is a user backed by a login session - `UserTokenContext`: when the identity is a user backed by an API token (i.e. macaroon) - `PublisherTokenContext`: when the identity is an OIDCPublisher backed by an API token Of these, User and UserTokenContext are confusable and prone to error. This change collapses them into a single UserContext type. * Add extra check * Update warehouse/accounts/security_policy.py --------- Co-authored-by: William Woodruff <[email protected]> Co-authored-by: Dustin Ingram <[email protected]>
1 parent 5debf46 commit 609871e

File tree

13 files changed

+122
-105
lines changed

13 files changed

+122
-105
lines changed

tests/unit/accounts/test_core.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
database_login_factory,
3030
)
3131
from warehouse.accounts.tasks import compute_user_metrics
32-
from warehouse.accounts.utils import UserTokenContext
32+
from warehouse.accounts.utils import UserContext
3333
from warehouse.oidc.interfaces import SignedClaims
3434
from warehouse.oidc.models import OIDCPublisher
3535
from warehouse.oidc.utils import PublisherTokenContext
@@ -40,15 +40,16 @@
4040

4141

4242
class TestUser:
43-
def test_with_user(self, db_request):
43+
def test_with_user_context_no_macaroon(self, db_request):
4444
user = UserFactory.create()
45-
request = pretend.stub(identity=user)
45+
user_ctx = UserContext(user, None)
46+
request = pretend.stub(identity=user_ctx)
4647

4748
assert accounts._user(request) is user
4849

49-
def test_with_user_token_context(self, db_request):
50+
def test_with_user_token_context_macaroon(self, db_request):
5051
user = UserFactory.create()
51-
user_ctx = UserTokenContext(user, pretend.stub())
52+
user_ctx = UserContext(user, pretend.stub())
5253
request = pretend.stub(identity=user_ctx)
5354

5455
assert accounts._user(request) is user
@@ -107,7 +108,7 @@ class TestOrganizationAccess:
107108
def test_organization_access(self, db_session, identity, flag, orgs, expected):
108109
user = None if not identity else UserFactory()
109110
request = pretend.stub(
110-
identity=user,
111+
identity=UserContext(user, None),
111112
find_service=lambda interface, context=None: pretend.stub(
112113
get_organizations_by_user=lambda x: orgs
113114
),

tests/unit/accounts/test_security_policy.py

+44-38
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pyramid.interfaces import ISecurityPolicy
1919
from zope.interface.verify import verifyClass
2020

21-
from warehouse.accounts import security_policy
21+
from warehouse.accounts import UserContext, security_policy
2222
from warehouse.accounts.interfaces import IUserService
2323
from warehouse.utils.security_policy import AuthenticationMethod
2424

@@ -451,7 +451,7 @@ def test_identity(self, monkeypatch):
451451
remote_addr="1.2.3.4",
452452
)
453453

454-
assert policy.identity(request) is user
454+
assert policy.identity(request).user is user
455455
assert request.authentication_method == AuthenticationMethod.SESSION
456456
assert session_helper_obj.authenticated_userid.calls == [pretend.call(request)]
457457
assert session_helper_cls.calls == [pretend.call()]
@@ -518,14 +518,15 @@ class TestPermits:
518518
"principals,expected", [("user:5", True), ("user:1", False)]
519519
)
520520
def test_acl(self, monkeypatch, policy_class, principals, expected):
521-
monkeypatch.setattr(security_policy, "User", pretend.stub)
522-
523521
request = pretend.stub(
524522
flags=pretend.stub(enabled=lambda flag: False),
525-
identity=pretend.stub(
526-
__principals__=lambda: principals,
527-
has_primary_verified_email=True,
528-
has_two_factor=True,
523+
identity=UserContext(
524+
user=pretend.stub(
525+
__principals__=lambda: principals,
526+
has_primary_verified_email=True,
527+
has_two_factor=True,
528+
),
529+
macaroon=None,
529530
),
530531
matched_route=pretend.stub(name="random.route"),
531532
)
@@ -535,13 +536,14 @@ def test_acl(self, monkeypatch, policy_class, principals, expected):
535536
assert bool(policy.permits(request, context, "myperm")) == expected
536537

537538
def test_permits_with_unverified_email(self, monkeypatch, policy_class):
538-
monkeypatch.setattr(security_policy, "User", pretend.stub)
539-
540539
request = pretend.stub(
541-
identity=pretend.stub(
542-
__principals__=lambda: ["user:5"],
543-
has_primary_verified_email=False,
544-
has_two_factor=False,
540+
identity=UserContext(
541+
user=pretend.stub(
542+
__principals__=lambda: ["user:5"],
543+
has_primary_verified_email=False,
544+
has_two_factor=False,
545+
),
546+
macaroon=None,
545547
),
546548
matched_route=pretend.stub(name="manage.projects"),
547549
)
@@ -551,13 +553,14 @@ def test_permits_with_unverified_email(self, monkeypatch, policy_class):
551553
assert not policy.permits(request, context, "myperm")
552554

553555
def test_permits_manage_projects_with_2fa(self, monkeypatch, policy_class):
554-
monkeypatch.setattr(security_policy, "User", pretend.stub)
555-
556556
request = pretend.stub(
557-
identity=pretend.stub(
558-
__principals__=lambda: ["user:5"],
559-
has_primary_verified_email=True,
560-
has_two_factor=True,
557+
identity=UserContext(
558+
user=pretend.stub(
559+
__principals__=lambda: ["user:5"],
560+
has_primary_verified_email=True,
561+
has_two_factor=True,
562+
),
563+
macaroon=None,
561564
),
562565
matched_route=pretend.stub(name="manage.projects"),
563566
)
@@ -567,14 +570,15 @@ def test_permits_manage_projects_with_2fa(self, monkeypatch, policy_class):
567570
assert policy.permits(request, context, "myperm")
568571

569572
def test_deny_manage_projects_without_2fa(self, monkeypatch, policy_class):
570-
monkeypatch.setattr(security_policy, "User", pretend.stub)
571-
572573
request = pretend.stub(
573574
flags=pretend.stub(enabled=lambda flag: False),
574-
identity=pretend.stub(
575-
__principals__=lambda: ["user:5"],
576-
has_primary_verified_email=True,
577-
has_two_factor=False,
575+
identity=UserContext(
576+
user=pretend.stub(
577+
__principals__=lambda: ["user:5"],
578+
has_primary_verified_email=True,
579+
has_two_factor=False,
580+
),
581+
macaroon=None,
578582
),
579583
matched_route=pretend.stub(name="manage.projects"),
580584
)
@@ -584,14 +588,15 @@ def test_deny_manage_projects_without_2fa(self, monkeypatch, policy_class):
584588
assert not policy.permits(request, context, "myperm")
585589

586590
def test_deny_forklift_file_upload_without_2fa(self, monkeypatch, policy_class):
587-
monkeypatch.setattr(security_policy, "User", pretend.stub)
588-
589591
request = pretend.stub(
590592
flags=pretend.stub(enabled=lambda flag: False),
591-
identity=pretend.stub(
592-
__principals__=lambda: ["user:5"],
593-
has_primary_verified_email=True,
594-
has_two_factor=False,
593+
identity=UserContext(
594+
user=pretend.stub(
595+
__principals__=lambda: ["user:5"],
596+
has_primary_verified_email=True,
597+
has_two_factor=False,
598+
),
599+
macaroon=None,
595600
),
596601
matched_route=pretend.stub(name="forklift.legacy.file_upload"),
597602
)
@@ -614,13 +619,14 @@ def test_deny_forklift_file_upload_without_2fa(self, monkeypatch, policy_class):
614619
def test_permits_2fa_routes_without_2fa(
615620
self, monkeypatch, policy_class, matched_route
616621
):
617-
monkeypatch.setattr(security_policy, "User", pretend.stub)
618-
619622
request = pretend.stub(
620-
identity=pretend.stub(
621-
__principals__=lambda: ["user:5"],
622-
has_primary_verified_email=True,
623-
has_two_factor=False,
623+
identity=UserContext(
624+
user=pretend.stub(
625+
__principals__=lambda: ["user:5"],
626+
has_primary_verified_email=True,
627+
has_two_factor=False,
628+
),
629+
macaroon=None,
624630
),
625631
matched_route=pretend.stub(name=matched_route),
626632
)

tests/unit/forklift/test_legacy.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from trove_classifiers import classifiers
3131
from webob.multidict import MultiDict
3232

33-
from warehouse.accounts.utils import UserTokenContext
33+
from warehouse.accounts.utils import UserContext
3434
from warehouse.admin.flags import AdminFlag, AdminFlagValue
3535
from warehouse.classifiers.models import Classifier
3636
from warehouse.forklift import legacy, metadata
@@ -968,7 +968,7 @@ def test_upload_escapes_nul_characters(self, pyramid_config, db_request):
968968

969969
assert "\x00" not in db_request.POST["summary"]
970970

971-
@pytest.mark.parametrize("token_context", [True, False])
971+
@pytest.mark.parametrize("macaroon_in_user_context", [True, False])
972972
@pytest.mark.parametrize(
973973
("digests",),
974974
[
@@ -997,7 +997,7 @@ def test_successful_upload(
997997
pyramid_config,
998998
db_request,
999999
digests,
1000-
token_context,
1000+
macaroon_in_user_context,
10011001
metrics,
10021002
):
10031003
monkeypatch.setattr(tempfile, "tempdir", str(tmpdir))
@@ -1013,11 +1013,10 @@ def test_successful_upload(
10131013
filename = f"{project.name}-{release.version}.tar.gz"
10141014

10151015
db_request.user = user
1016-
if token_context:
1017-
user_context = UserTokenContext(user, pretend.stub())
1018-
pyramid_config.testing_securitypolicy(identity=user_context)
1019-
else:
1020-
pyramid_config.testing_securitypolicy(identity=user)
1016+
user_context = UserContext(
1017+
user, pretend.stub() if macaroon_in_user_context else None
1018+
)
1019+
pyramid_config.testing_securitypolicy(identity=user_context)
10211020

10221021
db_request.user_agent = "warehouse-tests/6.6.6"
10231022

@@ -3977,7 +3976,7 @@ def test_upload_with_token_api_warns_if_trusted_publisher_configured(
39773976
[caveats.RequestUser(user_id=str(maintainer.id))],
39783977
user_id=maintainer.id,
39793978
)
3980-
identity = UserTokenContext(maintainer, macaroon)
3979+
identity = UserContext(maintainer, macaroon)
39813980
else:
39823981
claims = {"sha": "somesha"}
39833982
identity = PublisherTokenContext(publisher, SignedClaims(claims))

tests/unit/macaroons/test_caveats.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from pymacaroons import Macaroon
2121

2222
from warehouse.accounts import _oidc_publisher
23-
from warehouse.accounts.utils import UserTokenContext
23+
from warehouse.accounts.utils import UserContext
2424
from warehouse.macaroons import caveats
2525
from warehouse.macaroons.caveats import (
2626
Caveat,
@@ -271,21 +271,32 @@ def test_verify_no_identity(self):
271271

272272
assert result == Failure("token with user restriction without a user")
273273

274-
def test_verify_invalid_identity(self):
274+
def test_verify_invalid_identity_no_user(self):
275275
caveat = RequestUser(user_id="invalid")
276276
result = caveat.verify(
277277
pretend.stub(identity=pretend.stub()), pretend.stub(), pretend.stub()
278278
)
279279

280280
assert result == Failure("token with user restriction without a user")
281281

282+
def test_verify_invalid_identity_no_macaroon(self, db_request):
283+
user = UserFactory.create()
284+
user_context = UserContext(user, None)
285+
286+
caveat = RequestUser(user_id=str(user.id))
287+
result = caveat.verify(
288+
pretend.stub(identity=user_context), pretend.stub(), pretend.stub()
289+
)
290+
291+
assert result == Failure("token with user restriction without a macaroon")
292+
282293
def test_verify_invalid_user_id(self, db_request):
283294
user = UserFactory.create()
284-
user_token_context = UserTokenContext(user, pretend.stub())
295+
user_context = UserContext(user, pretend.stub())
285296

286297
caveat = RequestUser(user_id="invalid")
287298
result = caveat.verify(
288-
pretend.stub(identity=user_token_context), pretend.stub(), pretend.stub()
299+
pretend.stub(identity=user_context), pretend.stub(), pretend.stub()
289300
)
290301

291302
assert result == Failure(
@@ -294,11 +305,11 @@ def test_verify_invalid_user_id(self, db_request):
294305

295306
def test_verify_ok(self, db_request):
296307
user = UserFactory.create()
297-
user_token_context = UserTokenContext(user, pretend.stub())
308+
user_context = UserContext(user, pretend.stub())
298309

299310
caveat = RequestUser(user_id=str(user.id))
300311
result = caveat.verify(
301-
pretend.stub(identity=user_token_context), pretend.stub(), pretend.stub()
312+
pretend.stub(identity=user_context), pretend.stub(), pretend.stub()
302313
)
303314

304315
assert result == Success()

tests/unit/macaroons/test_security_policy.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from zope.interface.verify import verifyClass
2121

2222
from warehouse.accounts.interfaces import IUserService
23-
from warehouse.accounts.utils import UserTokenContext
23+
from warehouse.accounts.utils import UserContext
2424
from warehouse.authnz import Permissions
2525
from warehouse.macaroons import security_policy
2626
from warehouse.macaroons.interfaces import IMacaroonService
@@ -215,7 +215,7 @@ def test_identity_user(self, monkeypatch):
215215
),
216216
)
217217

218-
assert policy.identity(request) == UserTokenContext(user, macaroon)
218+
assert policy.identity(request) == UserContext(user, macaroon)
219219
assert extract_http_macaroon.calls == [pretend.call(request)]
220220
assert request.find_service.calls == [
221221
pretend.call(IMacaroonService, context=None),

tests/unit/macaroons/test_utils.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
import pretend
1414

1515
from tests.common.db.accounts import UserFactory
16-
from warehouse.accounts.utils import UserTokenContext
16+
from warehouse.accounts.utils import UserContext
1717
from warehouse.utils.security_policy import principals_for
1818

1919

20-
def test_usertoken_context_principals(db_request):
20+
def test_user_context_principals(db_request):
2121
user = UserFactory.create()
2222
assert principals_for(
23-
UserTokenContext(user=user, macaroon=pretend.stub())
23+
UserContext(user=user, macaroon=pretend.stub())
2424
) == principals_for(user)

tests/unit/utils/test_security_policy.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from zope.interface.verify import verifyClass
1717

1818
from tests.common.db.accounts import UserFactory
19-
from warehouse.accounts.utils import UserTokenContext
19+
from warehouse.accounts.utils import UserContext
2020
from warehouse.utils import security_policy
2121

2222

@@ -88,9 +88,9 @@ def test_authenticated_userid_nonuser_identity(self, db_request):
8888

8989
assert policy.authenticated_userid(request) is None
9090

91-
def test_authenticated_userid_user_token_context(self, db_request):
91+
def test_authenticated_userid_user_contex_macaroon(self, db_request):
9292
user = UserFactory.create()
93-
user_ctx = UserTokenContext(user, pretend.stub())
93+
user_ctx = UserContext(user, pretend.stub())
9494

9595
request = pretend.stub(add_finished_callback=lambda *a, **kw: None)
9696
subpolicies = [pretend.stub(identity=lambda r: user_ctx)]
@@ -102,11 +102,12 @@ def test_authenticated_userid_user_token_context(self, db_request):
102102
== str(user_ctx.user.id)
103103
)
104104

105-
def test_authenticated_userid(self, db_request):
105+
def test_authenticated_userid_user_context_no_macaroon(self, db_request):
106106
user = UserFactory.create()
107+
user_ctx = UserContext(user, None)
107108

108109
request = pretend.stub(add_finished_callback=lambda *a, **kw: None)
109-
subpolicies = [pretend.stub(identity=lambda r: user)]
110+
subpolicies = [pretend.stub(identity=lambda r: user_ctx)]
110111
policy = security_policy.MultiSecurityPolicy(subpolicies)
111112

112113
assert policy.authenticated_userid(request) == str(user.id)

warehouse/accounts/__init__.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
ITokenService,
1919
IUserService,
2020
)
21-
from warehouse.accounts.models import User
2221
from warehouse.accounts.security_policy import (
2322
BasicAuthSecurityPolicy,
2423
SessionSecurityPolicy,
@@ -32,7 +31,7 @@
3231
database_login_factory,
3332
)
3433
from warehouse.accounts.tasks import compute_user_metrics
35-
from warehouse.accounts.utils import UserTokenContext
34+
from warehouse.accounts.utils import UserContext
3635
from warehouse.admin.flags import AdminFlagValue
3736
from warehouse.macaroons.security_policy import MacaroonSecurityPolicy
3837
from warehouse.oidc.utils import PublisherTokenContext
@@ -55,12 +54,8 @@ def _user(request):
5554
if request.identity is None:
5655
return None
5756

58-
if isinstance(request.identity, UserTokenContext):
59-
# A UserTokenContext signals a user-created API token;
60-
# take the underlying user.
57+
if isinstance(request.identity, UserContext):
6158
return request.identity.user
62-
elif isinstance(request.identity, User):
63-
return request.identity
6459
else:
6560
return None
6661

0 commit comments

Comments
 (0)