From 74bc35de315e71fa3810c841531a5fe9000d8f4d Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 26 Oct 2021 23:28:29 +0200 Subject: [PATCH 1/3] Dependencies --- requirements/main.in | 2 +- requirements/main.txt | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/requirements/main.in b/requirements/main.in index 2cb91de7ea55..b657a450d01f 100644 --- a/requirements/main.in +++ b/requirements/main.in @@ -31,9 +31,9 @@ passlib>=1.6.4 premailer psycopg2 pycurl +pypitoken pyqrcode pyramid>=2.0 -pymacaroons pyramid_jinja2>=2.5 pyramid_mailer>=0.14.1 pyramid_multiauth diff --git a/requirements/main.txt b/requirements/main.txt index ca6c43276614..dd14e4e546bd 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -28,7 +28,9 @@ argon2-cffi==21.1.0 \ attrs==21.2.0 \ --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb - # via automat + # via + # automat + # jsonschema automat==20.2.0 \ --hash=sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33 \ --hash=sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111 @@ -559,6 +561,10 @@ jmespath==0.10.0 \ # via # boto3 # botocore +jsonschema==3.2.0 \ + --hash=sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 \ + --hash=sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a + # via pypitoken kombu==4.6.11 \ --hash=sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a \ --hash=sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74 @@ -816,7 +822,7 @@ pygments==2.10.0 \ pymacaroons==0.13.0 \ --hash=sha256:1e6bba42a5f66c245adf38a5a4006a99dcc06a0703786ea636098667d42903b8 \ --hash=sha256:3e14dff6a262fdbf1a15e769ce635a8aea72e6f8f91e408f9a97166c53b91907 - # via -r requirements/main.in + # via pypitoken pynacl==1.4.0 \ --hash=sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4 \ --hash=sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4 \ @@ -845,6 +851,10 @@ pyparsing==2.4.7 \ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b # via packaging +pypitoken==3.0.2 \ + --hash=sha256:1e92cb2e56965ffc18223aeb96bd3501755921512e290db6d947a7fb65afb021 \ + --hash=sha256:d8c560978b1fc2132dc9d2c7454cc3acedb92c2af7eaed789292417fdf73a588 + # via -r requirements/main.in pyqrcode==1.2.1 \ --hash=sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6 \ --hash=sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5 @@ -889,6 +899,29 @@ pyramid-tm==2.4 \ --hash=sha256:4a4e212cd239f06c496d074f5d294e88478b94059541448bc151d505f653be59 \ --hash=sha256:5fd6d4ac9181a65ec54e5b280229ed6d8b3ed6a8f5a0bcff05c572751f086533 # via -r requirements/main.in +pyrsistent==0.18.0 \ + --hash=sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2 \ + --hash=sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7 \ + --hash=sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea \ + --hash=sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426 \ + --hash=sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710 \ + --hash=sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1 \ + --hash=sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396 \ + --hash=sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2 \ + --hash=sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680 \ + --hash=sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35 \ + --hash=sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427 \ + --hash=sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b \ + --hash=sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b \ + --hash=sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f \ + --hash=sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef \ + --hash=sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c \ + --hash=sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4 \ + --hash=sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d \ + --hash=sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78 \ + --hash=sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b \ + --hash=sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72 + # via jsonschema python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 @@ -962,6 +995,7 @@ six==1.16.0 \ # google-cloud-storage # grpcio # html5lib + # jsonschema # limits # pymacaroons # pynacl @@ -1194,6 +1228,7 @@ setuptools==58.2.0 \ # -r requirements/main.in # google-api-core # google-auth + # jsonschema # pastedeploy # plaster # pyramid From 92de61556e6a95e55566afaadc9da03d4ff8e443 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Mon, 15 Mar 2021 18:59:59 +0100 Subject: [PATCH 2/3] Use pypitoken to generate, check, introspect tokens --- tests/functional/manage/test_views.py | 4 + tests/unit/macaroons/test_caveats.py | 125 ------------------ tests/unit/macaroons/test_models.py | 20 --- tests/unit/macaroons/test_services.py | 166 +++++++++++------------- tests/unit/manage/test_forms.py | 2 + tests/unit/manage/test_views.py | 60 +++++---- warehouse/integrations/github/utils.py | 2 +- warehouse/macaroons/caveats.py | 93 ------------- warehouse/macaroons/interfaces.py | 23 +++- warehouse/macaroons/models.py | 15 +-- warehouse/macaroons/services.py | 127 +++++++++--------- warehouse/manage/forms.py | 4 +- warehouse/manage/views.py | 21 +-- warehouse/templates/manage/account.html | 15 ++- warehouse/templates/manage/token.html | 10 +- warehouse/templates/pages/help.html | 2 +- 16 files changed, 233 insertions(+), 456 deletions(-) delete mode 100644 tests/unit/macaroons/test_caveats.py delete mode 100644 tests/unit/macaroons/test_models.py delete mode 100644 warehouse/macaroons/caveats.py diff --git a/tests/functional/manage/test_views.py b/tests/functional/manage/test_views.py index dec2e80d2ba1..18cea2579824 100644 --- a/tests/functional/manage/test_views.py +++ b/tests/functional/manage/test_views.py @@ -15,6 +15,8 @@ from webob.multidict import MultiDict from warehouse.accounts.interfaces import IPasswordBreachedService, IUserService +from warehouse.macaroons.interfaces import IMacaroonService +from warehouse.macaroons.services import database_macaroon_factory from warehouse.manage import views from ...common.db.accounts import EmailFactory, UserFactory @@ -27,6 +29,8 @@ def test_save_account(self, pyramid_services, user_service, db_request): pyramid_services.register_service( breach_service, IPasswordBreachedService, None ) + macaroon_service = database_macaroon_factory(context={}, request=db_request) + pyramid_services.register_service(macaroon_service, IMacaroonService, None) user = UserFactory.create(name="old name") EmailFactory.create(primary=True, verified=True, public=True, user=user) db_request.user = user diff --git a/tests/unit/macaroons/test_caveats.py b/tests/unit/macaroons/test_caveats.py deleted file mode 100644 index 052a6d9ede2a..000000000000 --- a/tests/unit/macaroons/test_caveats.py +++ /dev/null @@ -1,125 +0,0 @@ -# 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 json - -import pretend -import pytest - -from pymacaroons.exceptions import MacaroonInvalidSignatureException - -from warehouse.macaroons.caveats import Caveat, InvalidMacaroonError, V1Caveat, Verifier - -from ...common.db.packaging import ProjectFactory - - -class TestCaveat: - def test_creation(self): - verifier = pretend.stub() - caveat = Caveat(verifier) - - assert caveat.verifier is verifier - with pytest.raises(InvalidMacaroonError): - caveat.verify(pretend.stub()) - with pytest.raises(InvalidMacaroonError): - caveat(pretend.stub()) - - -class TestV1Caveat: - @pytest.mark.parametrize( - ["predicate", "result"], - [ - ("invalid json", False), - ('{"version": 2}', False), - ('{"permissions": null, "version": 1}', False), - ], - ) - def test_verify_invalid_predicates(self, predicate, result): - verifier = pretend.stub() - caveat = V1Caveat(verifier) - - with pytest.raises(InvalidMacaroonError): - caveat(predicate) - - def test_verify_valid_predicate(self): - verifier = pretend.stub() - caveat = V1Caveat(verifier) - predicate = '{"permissions": "user", "version": 1}' - - assert caveat(predicate) is True - - def test_verify_project_invalid_context(self): - verifier = pretend.stub(context=pretend.stub()) - caveat = V1Caveat(verifier) - - predicate = {"version": 1, "permissions": {"projects": ["notfoobar"]}} - with pytest.raises(InvalidMacaroonError): - caveat(json.dumps(predicate)) - - def test_verify_project_invalid_project_name(self, db_request): - project = ProjectFactory.create(name="foobar") - verifier = pretend.stub(context=project) - caveat = V1Caveat(verifier) - - predicate = {"version": 1, "permissions": {"projects": ["notfoobar"]}} - with pytest.raises(InvalidMacaroonError): - caveat(json.dumps(predicate)) - - def test_verify_project_no_projects_object(self, db_request): - project = ProjectFactory.create(name="foobar") - verifier = pretend.stub(context=project) - caveat = V1Caveat(verifier) - - predicate = { - "version": 1, - "permissions": {"somethingthatisntprojects": ["blah"]}, - } - with pytest.raises(InvalidMacaroonError): - caveat(json.dumps(predicate)) - - def test_verify_project(self, db_request): - project = ProjectFactory.create(name="foobar") - verifier = pretend.stub(context=project) - caveat = V1Caveat(verifier) - - predicate = {"version": 1, "permissions": {"projects": ["foobar"]}} - assert caveat(json.dumps(predicate)) is True - - -class TestVerifier: - def test_creation(self): - macaroon = pretend.stub() - context = pretend.stub() - principals = pretend.stub() - permission = pretend.stub() - verifier = Verifier(macaroon, context, principals, permission) - - assert verifier.macaroon is macaroon - assert verifier.context is context - assert verifier.principals is principals - assert verifier.permission is permission - - def test_verify(self, monkeypatch): - verify = pretend.call_recorder( - pretend.raiser(MacaroonInvalidSignatureException) - ) - macaroon = pretend.stub() - context = pretend.stub() - principals = pretend.stub() - permission = pretend.stub() - key = pretend.stub() - verifier = Verifier(macaroon, context, principals, permission) - - monkeypatch.setattr(verifier.verifier, "verify", verify) - with pytest.raises(InvalidMacaroonError): - verifier.verify(key) - assert verify.calls == [pretend.call(macaroon, key)] diff --git a/tests/unit/macaroons/test_models.py b/tests/unit/macaroons/test_models.py deleted file mode 100644 index 17fd9c4c1cf9..000000000000 --- a/tests/unit/macaroons/test_models.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -from warehouse.macaroons import models - - -def test_generate_key(): - key = models._generate_key() - - assert isinstance(key, bytes) - assert len(key) == 32 diff --git a/tests/unit/macaroons/test_services.py b/tests/unit/macaroons/test_services.py index 1a921d48ff14..57fae42e1e1e 100644 --- a/tests/unit/macaroons/test_services.py +++ b/tests/unit/macaroons/test_services.py @@ -10,24 +10,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import binascii -import struct - -from unittest import mock from uuid import uuid4 import pretend -import pymacaroons +import pypitoken import pytest -from pymacaroons.exceptions import MacaroonDeserializationException - from warehouse.macaroons import services from warehouse.macaroons.models import Macaroon from ...common.db.accounts import UserFactory +def test_generate_key(): + key = services._generate_key() + + assert isinstance(key, bytes) + assert len(key) == 32 + + def test_database_macaroon_factory(): db = pretend.stub() request = pretend.stub(db=db) @@ -43,18 +44,6 @@ def test_creation(self): assert service.db is session - @pytest.mark.parametrize( - ["raw_macaroon", "result"], - [ - (None, None), - ("noprefixhere", None), - ("invalid:prefix", None), - ("pypi-validprefix", "validprefix"), - ], - ) - def test_extract_raw_macaroon(self, macaroon_service, raw_macaroon, result): - assert macaroon_service._extract_raw_macaroon(raw_macaroon) == result - def test_find_macaroon_invalid_macaroon(self, macaroon_service): assert macaroon_service.find_macaroon(str(uuid4())) is None @@ -84,29 +73,23 @@ def test_find_from_raw(self, user_service, macaroon_service): "raw_macaroon", [ "pypi-aaaa", # Invalid macaroon - # Macaroon properly formatted but not found. The string is purposedly cut to - # avoid triggering the github token disclosure feature that this very - # function implements. - "py" - "pi-AgEIcHlwaS5vcmcCJGQ0ZDhhNzA2LTUxYTEtNDg0NC1hNDlmLTEyZDRiYzNkYjZmOQAABi" - "D6hJOpYl9jFI4jBPvA8gvV1mSu1Ic3xMHmxA4CSA2w_g", + pypitoken.Token.create( + domain="example.com", + identifier=str(uuid4()), + key=b"fake key", + ).dump(), ], ) def test_find_from_raw_not_found_or_invalid(self, macaroon_service, raw_macaroon): with pytest.raises(services.InvalidMacaroonError): macaroon_service.find_from_raw(raw_macaroon) - def test_find_userid_no_macaroon(self, macaroon_service): - assert macaroon_service.find_userid(None) is None - def test_find_userid_invalid_macaroon(self, macaroon_service): - raw_macaroon = pymacaroons.Macaroon( - location="fake location", + raw_macaroon = pypitoken.Token.create( + domain="example.com", identifier=str(uuid4()), key=b"fake key", - version=pymacaroons.MACAROON_V2, - ).serialize() - raw_macaroon = f"pypi-{raw_macaroon}" + ).dump() assert macaroon_service.find_userid(raw_macaroon) is None @@ -129,27 +112,18 @@ def test_find_userid(self, macaroon_service): assert user.id == user_id - def test_verify_unprefixed_macaroon(self, macaroon_service): - raw_macaroon = pymacaroons.Macaroon( - location="fake location", - identifier=str(uuid4()), - key=b"fake key", - version=pymacaroons.MACAROON_V2, - ).serialize() - + def test_verify_bad_macaroon(self, macaroon_service): with pytest.raises(services.InvalidMacaroonError): macaroon_service.verify( - raw_macaroon, pretend.stub(), pretend.stub(), pretend.stub() + "foo", pretend.stub(), pretend.stub(), pretend.stub() ) def test_verify_no_macaroon(self, macaroon_service): - raw_macaroon = pymacaroons.Macaroon( - location="fake location", + raw_macaroon = pypitoken.Token.create( + domain="example.com", identifier=str(uuid4()), key=b"fake key", - version=pymacaroons.MACAROON_V2, - ).serialize() - raw_macaroon = f"pypi-{raw_macaroon}" + ).dump() with pytest.raises(services.InvalidMacaroonError): macaroon_service.verify( @@ -158,59 +132,50 @@ def test_verify_no_macaroon(self, macaroon_service): def test_verify_invalid_macaroon(self, monkeypatch, user_service, macaroon_service): user = UserFactory.create() - raw_macaroon, _ = macaroon_service.create_macaroon( + raw_macaroon, dm = macaroon_service.create_macaroon( "fake location", user.id, "fake description", {"fake": "caveats"} ) - verifier_obj = pretend.stub(verify=pretend.call_recorder(lambda k: False)) - verifier_cls = pretend.call_recorder(lambda *a: verifier_obj) - monkeypatch.setattr(services, "Verifier", verifier_cls) + token_obj = pretend.stub( + check=pretend.call_recorder(pretend.raiser(pypitoken.ValidationError)), + identifier=str(dm.id), + prefix="pypi", + ) + token_cls = pretend.stub(load=lambda *a: token_obj) + + monkeypatch.setattr(pypitoken, "Token", token_cls) - context = pretend.stub() + context = pretend.stub(normalized_name="foo") principals = pretend.stub() permissions = pretend.stub() with pytest.raises(services.InvalidMacaroonError): macaroon_service.verify(raw_macaroon, context, principals, permissions) - assert verifier_cls.calls == [ - pretend.call(mock.ANY, context, principals, permissions) - ] + assert token_obj.check.calls == [pretend.call(key=dm.key, project="foo")] - def test_deserialize_raw_macaroon_when_none(self, macaroon_service): - raw_macaroon = pretend.stub() - macaroon_service._extract_raw_macaroon = pretend.call_recorder(lambda a: None) + def test_deserialize_raw_macaroon(self, macaroon_service): + token = pypitoken.Token.create(domain="pypi.org", identifier="b", key="c") + serialized = token.dump() - with pytest.raises(services.InvalidMacaroonError): - macaroon_service._deserialize_raw_macaroon(raw_macaroon) + result = macaroon_service._deserialize_raw_macaroon(serialized) - assert macaroon_service._extract_raw_macaroon.calls == [ - pretend.call(raw_macaroon), - ] + assert result.dump() == serialized - @pytest.mark.parametrize( - "exception", - [ - IndexError, - TypeError, - UnicodeDecodeError, - ValueError, - binascii.Error, - struct.error, - MacaroonDeserializationException, - Exception, # https://github.com/ecordell/pymacaroons/issues/50 - ], - ) - def test_deserialize_raw_macaroon(self, monkeypatch, macaroon_service, exception): - raw_macaroon = pretend.stub() - macaroon_service._extract_raw_macaroon = pretend.call_recorder( - lambda a: raw_macaroon - ) - monkeypatch.setattr( - pymacaroons.Macaroon, "deserialize", pretend.raiser(exception) + def test_deserialize_raw_macaroon_wrong_prefix(self, macaroon_service): + token = pypitoken.Token.create( + domain="pypi.org", identifier="b", key="c", prefix="wrong" ) + serialized = token.dump() + with pytest.raises(services.InvalidMacaroonError): + macaroon_service._deserialize_raw_macaroon(serialized) + + def test_deserialize_raw_macaroon_wrong_format(self, macaroon_service): + with pytest.raises(services.InvalidMacaroonError): + macaroon_service._deserialize_raw_macaroon("foo") + def test_deserialize_raw_macaroon_wrong_token(self, macaroon_service): with pytest.raises(services.InvalidMacaroonError): - macaroon_service._deserialize_raw_macaroon(raw_macaroon) + macaroon_service._deserialize_raw_macaroon("pypi-foo") def test_verify_malformed_macaroon(self, macaroon_service): with pytest.raises(services.InvalidMacaroonError): @@ -218,22 +183,24 @@ def test_verify_malformed_macaroon(self, macaroon_service): def test_verify_valid_macaroon(self, monkeypatch, macaroon_service): user = UserFactory.create() - raw_macaroon, _ = macaroon_service.create_macaroon( + raw_macaroon, dm = macaroon_service.create_macaroon( "fake location", user.id, "fake description", {"fake": "caveats"} ) - verifier_obj = pretend.stub(verify=pretend.call_recorder(lambda k: True)) - verifier_cls = pretend.call_recorder(lambda *a: verifier_obj) - monkeypatch.setattr(services, "Verifier", verifier_cls) + token_obj = pretend.stub( + check=pretend.call_recorder(lambda **a: None), + identifier=str(dm.id), + prefix="pypi", + ) + token_cls = pretend.stub(load=pretend.call_recorder(lambda *a: token_obj)) + monkeypatch.setattr(pypitoken, "Token", token_cls) - context = pretend.stub() + context = pretend.stub(normalized_name="foo") principals = pretend.stub() permissions = pretend.stub() assert macaroon_service.verify(raw_macaroon, context, principals, permissions) - assert verifier_cls.calls == [ - pretend.call(mock.ANY, context, principals, permissions) - ] + assert token_obj.check.calls == [pretend.call(key=dm.key, project="foo")] def test_delete_macaroon(self, user_service, macaroon_service): user = UserFactory.create() @@ -265,3 +232,18 @@ def test_get_macaroon_by_description(self, macaroon_service): macaroon_service.get_macaroon_by_description(user.id, macaroon.description) == dm ) + + @pytest.mark.parametrize( + "description", + [ + {}, + {"projects": ["baz", "yay"]}, + ], + ) + def test_describe_caveats(self, macaroon_service, description): + token = pypitoken.Token.create( + domain="example.com", identifier="foo", key="bar" + ) + token.restrict(**description) + caveat = token.restrictions[0].dump() + assert macaroon_service.describe_caveats(caveat) == description diff --git a/tests/unit/manage/test_forms.py b/tests/unit/manage/test_forms.py index 5173bef86002..a996ee92452e 100644 --- a/tests/unit/manage/test_forms.py +++ b/tests/unit/manage/test_forms.py @@ -437,6 +437,7 @@ def test_validate_token_scope_valid_user(self): ) assert form.validate() + assert form.validated_restrictions == {} def test_validate_token_scope_valid_project(self): form = forms.CreateMacaroonForm( @@ -447,6 +448,7 @@ def test_validate_token_scope_valid_project(self): ) assert form.validate() + assert form.validated_restrictions == {"projects": ["foo"]} class TestDeleteMacaroonForm: diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index 351c7a530dde..92828166deb8 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -69,12 +69,15 @@ class TestManageAccount: def test_default_response(self, monkeypatch, public_email, expected_public_email): breach_service = pretend.stub() user_service = pretend.stub() + describe_caveats = pretend.stub() + macaroon_service = pretend.stub(describe_caveats=describe_caveats) name = pretend.stub() user_id = pretend.stub() request = pretend.stub( find_service=lambda iface, **kw: { IPasswordBreachedService: breach_service, IUserService: user_service, + IMacaroonService: macaroon_service, }[iface], user=pretend.stub(name=name, id=user_id, public_email=public_email), ) @@ -99,6 +102,7 @@ def test_default_response(self, monkeypatch, public_email, expected_public_email "add_email_form": add_email_obj, "change_password_form": change_pass_obj, "active_projects": view.active_projects, + "describe_caveats": describe_caveats, } assert view.request == request assert view.user_service == user_service @@ -1726,12 +1730,13 @@ def test_default_response(self, monkeypatch): monkeypatch.setattr( views.ProvisionMacaroonViews, "project_names", project_names ) - + describe_caveats = pretend.stub() + macaroon_service = pretend.stub(describe_caveats=describe_caveats) request = pretend.stub( user=pretend.stub(id=pretend.stub(), username=pretend.stub()), find_service=lambda interface, **kw: { - IMacaroonService: pretend.stub(), IUserService: pretend.stub(), + IMacaroonService: macaroon_service, }[interface], ) @@ -1741,6 +1746,7 @@ def test_default_response(self, monkeypatch): "project_names": project_names, "create_macaroon_form": create_macaroon_obj, "delete_macaroon_form": delete_macaroon_obj, + "describe_caveats": describe_caveats, } def test_project_names(self, db_request): @@ -1847,7 +1853,8 @@ def test_create_macaroon_invalid_form(self, monkeypatch): assert macaroon_service.create_macaroon.calls == [] def test_create_macaroon(self, monkeypatch): - macaroon = pretend.stub() + caveats = {"foo": "bar"} + macaroon = pretend.stub(caveats=caveats) macaroon_service = pretend.stub( create_macaroon=pretend.call_recorder( lambda *a, **kw: ("not a real raw macaroon", macaroon) @@ -1870,7 +1877,7 @@ def test_create_macaroon(self, monkeypatch): create_macaroon_obj = pretend.stub( validate=lambda: True, description=pretend.stub(data=pretend.stub()), - validated_scope="foobar", + validated_restrictions={}, ) create_macaroon_cls = pretend.call_recorder( lambda *a, **kw: create_macaroon_obj @@ -1892,13 +1899,10 @@ def test_create_macaroon(self, monkeypatch): assert macaroon_service.create_macaroon.calls == [ pretend.call( - location=request.domain, + domain=request.domain, user_id=request.user.id, description=create_macaroon_obj.description.data, - caveats={ - "permissions": create_macaroon_obj.validated_scope, - "version": 1, - }, + restrictions={}, ) ] assert result == { @@ -1914,16 +1918,14 @@ def test_create_macaroon(self, monkeypatch): ip_address=request.remote_addr, additional={ "description": create_macaroon_obj.description.data, - "caveats": { - "permissions": create_macaroon_obj.validated_scope, - "version": 1, - }, + "caveats": {"foo": "bar"}, }, ) ] def test_create_macaroon_records_events_for_each_project(self, monkeypatch): - macaroon = pretend.stub() + caveats = pretend.stub() + macaroon = pretend.stub(caveats=caveats) macaroon_service = pretend.stub( create_macaroon=pretend.call_recorder( lambda *a, **kw: ("not a real raw macaroon", macaroon) @@ -1953,7 +1955,7 @@ def test_create_macaroon_records_events_for_each_project(self, monkeypatch): create_macaroon_obj = pretend.stub( validate=lambda: True, description=pretend.stub(data=pretend.stub()), - validated_scope={"projects": ["foo", "bar"]}, + validated_restrictions={"projects": ["foo", "bar"]}, ) create_macaroon_cls = pretend.call_recorder( lambda *a, **kw: create_macaroon_obj @@ -1975,12 +1977,11 @@ def test_create_macaroon_records_events_for_each_project(self, monkeypatch): assert macaroon_service.create_macaroon.calls == [ pretend.call( - location=request.domain, + domain=request.domain, user_id=request.user.id, description=create_macaroon_obj.description.data, - caveats={ - "permissions": create_macaroon_obj.validated_scope, - "version": 1, + restrictions={ + "projects": ["foo", "bar"], }, ) ] @@ -1997,10 +1998,7 @@ def test_create_macaroon_records_events_for_each_project(self, monkeypatch): ip_address=request.remote_addr, additional={ "description": create_macaroon_obj.description.data, - "caveats": { - "permissions": create_macaroon_obj.validated_scope, - "version": 1, - }, + "caveats": caveats, }, ), pretend.call( @@ -2087,12 +2085,11 @@ def test_delete_macaroon_dangerous_redirect(self, monkeypatch): assert macaroon_service.delete_macaroon.calls == [] def test_delete_macaroon(self, monkeypatch): - macaroon = pretend.stub( - description="fake macaroon", caveats={"version": 1, "permissions": "user"} - ) + macaroon = pretend.stub(description="fake macaroon", caveats=pretend.stub()) macaroon_service = pretend.stub( delete_macaroon=pretend.call_recorder(lambda id: pretend.stub()), find_macaroon=pretend.call_recorder(lambda id: macaroon), + describe_caveats=pretend.call_recorder(lambda mac: {}), ) record_event = pretend.call_recorder( pretend.call_recorder(lambda *a, **kw: None) @@ -2132,6 +2129,9 @@ def test_delete_macaroon(self, monkeypatch): assert macaroon_service.find_macaroon.calls == [ pretend.call(delete_macaroon_obj.macaroon_id.data) ] + assert macaroon_service.describe_caveats.calls == [ + pretend.call(macaroon.caveats) + ] assert request.session.flash.calls == [ pretend.call("Deleted API token 'fake macaroon'.", queue="success") ] @@ -2147,11 +2147,14 @@ def test_delete_macaroon(self, monkeypatch): def test_delete_macaroon_records_events_for_each_project(self, monkeypatch): macaroon = pretend.stub( description="fake macaroon", - caveats={"version": 1, "permissions": {"projects": ["foo", "bar"]}}, + caveats=pretend.stub(), ) macaroon_service = pretend.stub( delete_macaroon=pretend.call_recorder(lambda id: pretend.stub()), find_macaroon=pretend.call_recorder(lambda id: macaroon), + describe_caveats=pretend.call_recorder( + lambda mac: {"projects": ["foo", "bar"]} + ), ) record_event = pretend.call_recorder( pretend.call_recorder(lambda *a, **kw: None) @@ -2198,6 +2201,9 @@ def test_delete_macaroon_records_events_for_each_project(self, monkeypatch): assert macaroon_service.find_macaroon.calls == [ pretend.call(delete_macaroon_obj.macaroon_id.data) ] + assert macaroon_service.describe_caveats.calls == [ + pretend.call(macaroon.caveats) + ] assert request.session.flash.calls == [ pretend.call("Deleted API token 'fake macaroon'.", queue="success") ] diff --git a/warehouse/integrations/github/utils.py b/warehouse/integrations/github/utils.py index 23345f14ba77..0593ad7ec102 100644 --- a/warehouse/integrations/github/utils.py +++ b/warehouse/integrations/github/utils.py @@ -21,8 +21,8 @@ from warehouse import integrations from warehouse.accounts.interfaces import IUserService from warehouse.email import send_token_compromised_email_leak -from warehouse.macaroons.caveats import InvalidMacaroonError from warehouse.macaroons.interfaces import IMacaroonService +from warehouse.macaroons.services import InvalidMacaroonError from warehouse.metrics import IMetricsService diff --git a/warehouse/macaroons/caveats.py b/warehouse/macaroons/caveats.py deleted file mode 100644 index accb743b8a3d..000000000000 --- a/warehouse/macaroons/caveats.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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 json - -import pymacaroons - -from warehouse.packaging.models import Project - - -class InvalidMacaroonError(Exception): - ... - - -class Caveat: - def __init__(self, verifier): - self.verifier = verifier - - def verify(self, predicate): - raise InvalidMacaroonError - - def __call__(self, predicate): - return self.verify(predicate) - - -class V1Caveat(Caveat): - def verify_projects(self, projects): - # First, ensure that we're actually operating in - # the context of a package. - if not isinstance(self.verifier.context, Project): - raise InvalidMacaroonError( - "project-scoped token used outside of a project context" - ) - - project = self.verifier.context - if project.normalized_name in projects: - return True - - raise InvalidMacaroonError( - f"project-scoped token is not valid for project '{project.name}'" - ) - - def verify(self, predicate): - try: - data = json.loads(predicate) - except ValueError: - raise InvalidMacaroonError("malformatted predicate") - - if data.get("version") != 1: - raise InvalidMacaroonError("invalidate version in predicate") - - permissions = data.get("permissions") - if permissions is None: - raise InvalidMacaroonError("invalid permissions in predicate") - - if permissions == "user": - # User-scoped tokens behave exactly like a user's normal credentials. - return True - - projects = permissions.get("projects") - if projects is None: - raise InvalidMacaroonError("invalid projects in predicate") - - return self.verify_projects(projects) - - -class Verifier: - def __init__(self, macaroon, context, principals, permission): - self.macaroon = macaroon - self.context = context - self.principals = principals - self.permission = permission - self.verifier = pymacaroons.Verifier() - - def verify(self, key): - self.verifier.satisfy_general(V1Caveat(self)) - - try: - return self.verifier.verify(self.macaroon, key) - except ( - pymacaroons.exceptions.MacaroonInvalidSignatureException, - Exception, # https://github.com/ecordell/pymacaroons/issues/50 - ): - raise InvalidMacaroonError("invalid macaroon signature") diff --git a/warehouse/macaroons/interfaces.py b/warehouse/macaroons/interfaces.py index ba2ddfa942e1..4bde3c7e0a95 100644 --- a/warehouse/macaroons/interfaces.py +++ b/warehouse/macaroons/interfaces.py @@ -26,7 +26,7 @@ def _extract_raw_macaroon(raw_macaroon): def find_from_raw(raw_macaroon): """ Returns a macaroon model from the DB from a raw macaroon, or raises - InvalidMacaroon if not found or for malformed macaroons. + InvalidMacaroonError if not found or for malformed macaroons. """ def find_macaroon(macaroon_id): @@ -49,10 +49,16 @@ def verify(raw_macaroon, context, principals, permission): Raises InvalidMacaroon if the macaroon is not valid. """ - def create_macaroon(location, user_id, description, caveats): + def create_macaroon(domain, user_id, description, restrictions): """ - Returns a new raw (serialized) macaroon. The description provided - is not embedded into the macaroon, only stored in the DB model. + Returns a tuple with: + + - the raw (serialized) macaroon string, + - the database macaroon + + The description provided is not embedded into the macaroon, only stored in the + DB model. + Restrictions has the same format as in describe_caveats. """ def delete_macaroon(macaroon_id): @@ -67,3 +73,12 @@ def get_macaroon_by_description(user_id, description): Returns None if the user doesn't have a macaroon with this description. """ + + def describe_caveats(self, caveats): + """ + Given a value of macaroon caveats (like the one stored in DB), return a + description of the caveats, as a dict: + + - No key: no restriction (user-wide token) + - "projects" key: contains a list of normalized project names. + """ diff --git a/warehouse/macaroons/models.py b/warehouse/macaroons/models.py index 33373b7aa9e0..65473e7928bd 100644 --- a/warehouse/macaroons/models.py +++ b/warehouse/macaroons/models.py @@ -10,8 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - from sqlalchemy import ( Column, DateTime, @@ -26,10 +24,6 @@ from warehouse import db -def _generate_key(): - return os.urandom(32) - - class Macaroon(db.Model): __tablename__ = "macaroons" @@ -55,11 +49,4 @@ class Macaroon(db.Model): # scope of their macaroon is. caveats = Column(JSONB, nullable=False, server_default=sql.text("'{}'")) - # It might be better to move this default into the database, that way we - # make it less likely that something does it incorrectly (since the - # default would be to generate a random key). However, it appears the - # PostgreSQL pgcrypto extension uses OpenSSL RAND_bytes if available - # instead of urandom. This is less than optimal, and we would generally - # prefer to just always use urandom. Thus we'll do this ourselves here - # in our application. - key = Column(LargeBinary, nullable=False, default=_generate_key) + key = Column(LargeBinary, nullable=False) diff --git a/warehouse/macaroons/services.py b/warehouse/macaroons/services.py index c394b74f8349..4d1a9226a826 100644 --- a/warehouse/macaroons/services.py +++ b/warehouse/macaroons/services.py @@ -11,43 +11,35 @@ # limitations under the License. import datetime -import json +import os import uuid -import pymacaroons +import pypitoken -from pymacaroons.exceptions import MacaroonDeserializationException from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import NoResultFound from zope.interface import implementer from warehouse.accounts.models import User -from warehouse.macaroons.caveats import InvalidMacaroonError, Verifier from warehouse.macaroons.interfaces import IMacaroonService from warehouse.macaroons.models import Macaroon -@implementer(IMacaroonService) -class DatabaseMacaroonService: - def __init__(self, db_session): - self.db = db_session +def _generate_key(): + return os.urandom(32) - def _extract_raw_macaroon(self, prefixed_macaroon): - """ - Returns the base64-encoded macaroon component of a PyPI macaroon, - dropping the prefix. - Returns None if the macaroon is None, has no prefix, or has the - wrong prefix. - """ - if prefixed_macaroon is None: - return None +class InvalidMacaroonError(Exception): + ... - prefix, _, raw_macaroon = prefixed_macaroon.partition("-") - if prefix != "pypi" or not raw_macaroon: - return None - return raw_macaroon +TOKEN_PREFIX = "pypi" + + +@implementer(IMacaroonService) +class DatabaseMacaroonService: + def __init__(self, db_session): + self.db = db_session def find_macaroon(self, macaroon_id): """ @@ -67,34 +59,27 @@ def find_macaroon(self, macaroon_id): return dm def _deserialize_raw_macaroon(self, raw_macaroon): - raw_macaroon = self._extract_raw_macaroon(raw_macaroon) - - if raw_macaroon is None: - raise InvalidMacaroonError("malformed or nonexistent macaroon") - try: - return pymacaroons.Macaroon.deserialize(raw_macaroon) - except ( - MacaroonDeserializationException, - Exception, # https://github.com/ecordell/pymacaroons/issues/50 - ): - raise InvalidMacaroonError("malformed macaroon") + token = pypitoken.Token.load(raw_macaroon) + except pypitoken.LoaderError as exc: + raise InvalidMacaroonError(str(exc)) + if token.prefix != TOKEN_PREFIX: + raise InvalidMacaroonError( + f"Token has wrong prefix: {token.prefix} (expected {TOKEN_PREFIX}" + ) + # We don't check the domain because it's checks as part of the signature check + return token def find_userid(self, raw_macaroon): """ Returns the id of the user associated with the given raw (serialized) - macaroon. + macaroon or None. """ try: - m = self._deserialize_raw_macaroon(raw_macaroon) + dm = self.find_from_raw(raw_macaroon) except InvalidMacaroonError: return None - dm = self.find_macaroon(m.identifier.decode()) - - if dm is None: - return None - return dm.user.id def find_from_raw(self, raw_macaroon): @@ -102,7 +87,7 @@ def find_from_raw(self, raw_macaroon): Returns a DB macaroon matching the imput, or raises InvalidMacaroonError """ m = self._deserialize_raw_macaroon(raw_macaroon) - dm = self.find_macaroon(m.identifier.decode()) + dm = self.find_macaroon(m.identifier) if not dm: raise InvalidMacaroonError("Macaroon not found") return dm @@ -114,20 +99,23 @@ def verify(self, raw_macaroon, context, principals, permission): Raises InvalidMacaroonError if the macaroon is not valid. """ - m = self._deserialize_raw_macaroon(raw_macaroon) - dm = self.find_macaroon(m.identifier.decode()) + token = self._deserialize_raw_macaroon(raw_macaroon) + dm = self.find_macaroon(token.identifier) if dm is None: raise InvalidMacaroonError("deleted or nonexistent macaroon") - verifier = Verifier(m, context, principals, permission) - if verifier.verify(dm.key): - dm.last_used = datetime.datetime.now() - return True + project = context.normalized_name + + try: + token.check(key=dm.key, project=project) + except pypitoken.ValidationError as exc: + raise InvalidMacaroonError(str(exc)) - raise InvalidMacaroonError("invalid macaroon") + dm.last_used = datetime.datetime.now() + return True - def create_macaroon(self, location, user_id, description, caveats): + def create_macaroon(self, domain, user_id, description, restrictions): """ Returns a tuple of a new raw (serialized) macaroon and its DB model. The description provided is not embedded into the macaroon, only stored @@ -135,19 +123,42 @@ def create_macaroon(self, location, user_id, description, caveats): """ user = self.db.query(User).filter(User.id == user_id).one() - dm = Macaroon(user=user, description=description, caveats=caveats) + identifier = uuid.uuid4() + key = _generate_key() + + token = pypitoken.Token.create( + domain=domain, identifier=str(identifier), key=key, prefix="pypi" + ) + # even if projects is None, this will create a NoopRestriction. With + # the current implementation, we need to always have a restriction in place. + token.restrict( + # We're likely to copy restrictions into this function kwargs as-is, but + # it's good to avoid **restrictions here to maintain the abstraction layer. + # If something break because the pypitoken lib expects new arguments, we'd + # rather it fails here and be sure to see it in the tests. + projects=restrictions.get("projects", None), + ) + + dm = Macaroon( + id=identifier, + user=user, + key=key, + description=description, + caveats=token.restrictions[0].dump(), + ) self.db.add(dm) self.db.flush() - m = pymacaroons.Macaroon( - location=location, - identifier=str(dm.id), - key=dm.key, - version=pymacaroons.MACAROON_V2, - ) - m.add_first_party_caveat(json.dumps(caveats)) - serialized_macaroon = f"pypi-{m.serialize()}" - return serialized_macaroon, dm + return token.dump(), dm + + def describe_caveats(self, caveats): + description = {} + restriction = pypitoken.Restriction.load(caveats) + + if isinstance(restriction, pypitoken.ProjectsRestriction): + description["projects"] = restriction.projects + + return description def delete_macaroon(self, macaroon_id): """ diff --git a/warehouse/manage/forms.py b/warehouse/manage/forms.py index 4bda8d26ab32..1b4216575d14 100644 --- a/warehouse/manage/forms.py +++ b/warehouse/manage/forms.py @@ -252,7 +252,7 @@ def validate_token_scope(self, field): raise wtforms.ValidationError("Specify the token scope") if scope_kind == "user": - self.validated_scope = scope_kind + self.validated_restrictions = {} return try: @@ -267,7 +267,7 @@ def validate_token_scope(self, field): f"Unknown or invalid project name: {scope_value}" ) - self.validated_scope = {"projects": [scope_value]} + self.validated_restrictions = {"projects": [scope_value]} class DeleteMacaroonForm(UsernameMixin, PasswordMixin, forms.Form): diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py index 585998e81ce0..edc942fab99b 100644 --- a/warehouse/manage/views.py +++ b/warehouse/manage/views.py @@ -131,6 +131,7 @@ def __init__(self, request): self.breach_service = request.find_service( IPasswordBreachedService, context=None ) + self.macaroon_service = request.find_service(IMacaroonService, context=None) @property def active_projects(self): @@ -154,6 +155,7 @@ def default_response(self): breach_service=self.breach_service, ), "active_projects": self.active_projects, + "describe_caveats": self.macaroon_service.describe_caveats, } @view_config(request_method="GET") @@ -798,6 +800,7 @@ def default_response(self): user_service=self.user_service, macaroon_service=self.macaroon_service, ), + "describe_caveats": self.macaroon_service.describe_caveats, } @view_config(request_method="GET") @@ -821,12 +824,12 @@ def create_macaroon(self): response = {**self.default_response} if form.validate(): - macaroon_caveats = {"permissions": form.validated_scope, "version": 1} + restrictions = form.validated_restrictions serialized_macaroon, macaroon = self.macaroon_service.create_macaroon( - location=self.request.domain, + domain=self.request.domain, user_id=self.request.user.id, description=form.description.data, - caveats=macaroon_caveats, + restrictions=restrictions, ) self.user_service.record_event( self.request.user.id, @@ -834,14 +837,14 @@ def create_macaroon(self): ip_address=self.request.remote_addr, additional={ "description": form.description.data, - "caveats": macaroon_caveats, + "caveats": macaroon.caveats, }, ) - if "projects" in form.validated_scope: + if "projects" in restrictions: projects = [ project for project in self.request.user.projects - if project.normalized_name in form.validated_scope["projects"] + if project.normalized_name in restrictions["projects"] ] for project in projects: # NOTE: We don't disclose the full caveats for this token @@ -885,12 +888,12 @@ def delete_macaroon(self): ip_address=self.request.remote_addr, additional={"macaroon_id": form.macaroon_id.data}, ) - if "projects" in macaroon.caveats["permissions"]: + restrictions = self.macaroon_service.describe_caveats(macaroon.caveats) + if "projects" in restrictions: projects = [ project for project in self.request.user.projects - if project.normalized_name - in macaroon.caveats["permissions"]["projects"] + if project.normalized_name in restrictions["projects"] ] for project in projects: project.record_event( diff --git a/warehouse/templates/manage/account.html b/warehouse/templates/manage/account.html index bb9692c2aba3..b25e2d0ec388 100644 --- a/warehouse/templates/manage/account.html +++ b/warehouse/templates/manage/account.html @@ -158,6 +158,7 @@ {% endmacro %} {% macro api_row(macaroon) -%} + {% set restrictions = describe_caveats(macaroon.caveats) %} {% trans %}Name{% endtrans %} @@ -165,10 +166,10 @@ {% trans %}Scope{% endtrans %} - {% if macaroon.caveats.get("permissions") == 'user' %} + {% if not restrictions %} {% trans %}All projects{% endtrans %} {% else %} - {% for project in macaroon.caveats.get("permissions")['projects'] %} + {% for project in restrictions.projects %} {{ project }} {% endfor %} {% endif %} @@ -687,13 +688,14 @@

{% trans %}Security history{% endtrans %}

{% elif event.tag == "account:api_token:added" %} + {% set restrictions = describe_caveats(event.additional.caveats) %} {% trans %}API token added{% endtrans %}
{% trans %}Token name:{% endtrans %} {{ event.additional.description }}
- {% if event.additional.caveats.permissions == "user" %} + {% if not restrictions %} {% trans %}Token scope: entire account{% endtrans %} {% else %} - {% trans project_name=event.additional.caveats.permissions.projects[0] %}Token scope: Project {{ project_name }}{% endtrans %} + {% trans project_name=restrictions.projects[0] %}Token scope: Project {{ project_name }}{% endtrans %} {% endif %}
@@ -702,14 +704,15 @@

{% trans %}Security history{% endtrans %}

{% trans %}Unique identifier:{% endtrans %} {{ event.additional.macaroon_id }} {% elif event.tag == "account:api_token:removed_leak" %} + {% set restrictions = describe_caveats(event.additional.caveats) %} {% trans %}API token automatically removed for security reasons{% endtrans %}
{% trans %}Token name:{% endtrans %} {{ event.additional.description }}
{% trans %}Unique identifier:{% endtrans %} {{ event.additional.macaroon_id }}
- {% if event.additional.permissions == "user" %} + {% if not restrictions %} {% trans %}Token scope: entire account{% endtrans %} {% else %} - {% trans project_name=event.additional.permissions.projects[0] %}Token scope: Project {{ project_name }}{% endtrans %} + {% trans project_name=restrictions.projects[0] %}Token scope: Project {{ project_name }}{% endtrans %} {% endif %}
{% trans public_url=event.additional.public_url %}Reason: Token found at public url{% endtrans %}
diff --git a/warehouse/templates/manage/token.html b/warehouse/templates/manage/token.html index a3434d17ad38..3fefb5b9432b 100644 --- a/warehouse/templates/manage/token.html +++ b/warehouse/templates/manage/token.html @@ -34,14 +34,16 @@

{{ title }}

{% if serialized_macaroon %} + {% set restrictions = describe_caveats(macaroon.caveats) %}

{% trans macaroon_description=macaroon.description %}Token for "{{ macaroon_description }}"{% endtrans %}

{% trans %}Permissions:{% endtrans %} {% trans %}Upload packages{% endtrans %}
- {% if macaroon.caveats.permissions == "user" %} - {% trans %}Scope:{% endtrans %} {% trans %}Entire account (all projects){% endtrans %} + {% trans %}Scope:{% endtrans %} + {% if not restrictions %} + {% trans %}Entire account (all projects){% endtrans %} {% else %} - {% trans %}Scope:{% endtrans %} {% trans project=macaroon.caveats.permissions.projects[0] %}Project "{{ project }}"{% endtrans %} + {% trans project=restrictions.projects[0] %}Project "{{ project }}"{% endtrans %} {% endif %}

@@ -78,7 +80,7 @@

{% trans %}Using this token{% endtrans %}

  • {% trans prefix='pypi-' %}Set your password to the token value, including the {{ prefix }} prefix{% endtrans %}
  • - {% if macaroon.caveats.permissions == "user" %} + {% if not restrictions %}

    {% trans trimmed href='https://pypi.org/project/twine/', filename='$HOME/.pypirc' %} diff --git a/warehouse/templates/pages/help.html b/warehouse/templates/pages/help.html index 5734616d2f76..423006fb9311 100644 --- a/warehouse/templates/pages/help.html +++ b/warehouse/templates/pages/help.html @@ -499,7 +499,7 @@

    {{ apitoken() }}

    Where you edit or add these values will depend on your individual use case. For example, some users may need to edit their .pypirc file, while others may need to update their CI configuration file (e.g. .travis.yml if you are using Travis). {% endtrans %}

    -

    {% trans %}Advanced users may wish to inspect their token by decoding it with base64, and checking the output against the unique identifier displayed on PyPI.{% endtrans %}

    +

    {% trans trimmed pypitoken_href='https://pypitoken.readthedocs.io/', title=gettext('External link') %}Advanced users may wish to inspect their token by decoding it with the pypitoken library, and checking the identifier against the unique identifier displayed on PyPI. Please make sure to keep your token identifier private, though. Knowing just the identifier of a token is not enough to impersonate the user, but it's sufficient to have a token be disabled.{% endtrans %}

    {{ sensitiveactions() }}

    From 075251bb5c664b7af07927a2a81348c1e60a4109 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Sat, 20 Mar 2021 00:16:18 +0100 Subject: [PATCH 3/3] translations --- warehouse/locale/messages.pot | 431 +++++++++++++++++----------------- 1 file changed, 218 insertions(+), 213 deletions(-) diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 4c52ece0a84b..26d35baa4bc9 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -208,55 +208,55 @@ msgstr "" msgid "Banner Preview" msgstr "" -#: warehouse/manage/views.py:207 +#: warehouse/manage/views.py:209 msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views.py:697 warehouse/manage/views.py:733 +#: warehouse/manage/views.py:699 warehouse/manage/views.py:735 msgid "" "You must provision a two factor method before recovery codes can be " "generated" msgstr "" -#: warehouse/manage/views.py:708 +#: warehouse/manage/views.py:710 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views.py:709 +#: warehouse/manage/views.py:711 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views.py:761 +#: warehouse/manage/views.py:763 msgid "Invalid credentials. Try again" msgstr "" -#: warehouse/manage/views.py:1511 +#: warehouse/manage/views.py:1514 msgid "User '${username}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views.py:1522 +#: warehouse/manage/views.py:1525 msgid "" "User '${username}' does not have a verified primary email address and " "cannot be added as a ${role_name} for project" msgstr "" -#: warehouse/manage/views.py:1535 +#: warehouse/manage/views.py:1538 msgid "User '${username}' already has an active invite. Please try again later." msgstr "" -#: warehouse/manage/views.py:1593 +#: warehouse/manage/views.py:1596 msgid "Invitation sent to '${username}'" msgstr "" -#: warehouse/manage/views.py:1640 +#: warehouse/manage/views.py:1643 msgid "Could not find role invitation." msgstr "" -#: warehouse/manage/views.py:1651 +#: warehouse/manage/views.py:1654 msgid "Invitation already expired." msgstr "" -#: warehouse/manage/views.py:1675 +#: warehouse/manage/views.py:1678 msgid "Invitation revoked from '${username}'." msgstr "" @@ -301,8 +301,8 @@ msgstr "" #: warehouse/templates/includes/packaging/project-data.html:66 #: warehouse/templates/index.html:112 warehouse/templates/index.html:116 #: warehouse/templates/manage/account.html:153 -#: warehouse/templates/manage/account.html:252 -#: warehouse/templates/manage/account.html:258 +#: warehouse/templates/manage/account.html:253 +#: warehouse/templates/manage/account.html:259 #: warehouse/templates/manage/account/totp-provision.html:32 #: warehouse/templates/manage/account/webauthn-provision.html:26 #: warehouse/templates/manage/account/webauthn-provision.html:28 @@ -336,6 +336,7 @@ msgstr "" #: warehouse/templates/pages/help.html:434 #: warehouse/templates/pages/help.html:440 #: warehouse/templates/pages/help.html:498 +#: warehouse/templates/pages/help.html:502 #: warehouse/templates/pages/help.html:518 #: warehouse/templates/pages/help.html:524 #: warehouse/templates/pages/help.html:527 @@ -522,7 +523,7 @@ msgstr "" #: warehouse/templates/base.html:162 warehouse/templates/base.html:171 #: warehouse/templates/base.html:181 #: warehouse/templates/includes/session-notifications.html:19 -#: warehouse/templates/manage/account.html:789 +#: warehouse/templates/manage/account.html:792 #: warehouse/templates/manage/documentation.html:27 #: warehouse/templates/manage/manage_base.html:106 #: warehouse/templates/manage/manage_base.html:158 @@ -795,7 +796,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:140 #: warehouse/templates/accounts/reset-password.html:60 #: warehouse/templates/accounts/reset-password.html:65 -#: warehouse/templates/manage/account.html:429 +#: warehouse/templates/manage/account.html:430 #: warehouse/templates/re-auth.html:18 warehouse/templates/re-auth.html:68 msgid "Confirm password" msgstr "" @@ -824,18 +825,18 @@ msgstr "" #: warehouse/templates/accounts/reset-password.html:40 #: warehouse/templates/accounts/reset-password.html:62 #: warehouse/templates/accounts/two-factor.html:89 -#: warehouse/templates/manage/account.html:294 -#: warehouse/templates/manage/account.html:311 -#: warehouse/templates/manage/account.html:368 -#: warehouse/templates/manage/account.html:393 -#: warehouse/templates/manage/account.html:410 -#: warehouse/templates/manage/account.html:426 +#: warehouse/templates/manage/account.html:295 +#: warehouse/templates/manage/account.html:312 +#: warehouse/templates/manage/account.html:369 +#: warehouse/templates/manage/account.html:394 +#: warehouse/templates/manage/account.html:411 +#: warehouse/templates/manage/account.html:427 #: warehouse/templates/manage/account/totp-provision.html:69 #: warehouse/templates/manage/account/webauthn-provision.html:44 #: warehouse/templates/manage/roles.html:170 #: warehouse/templates/manage/roles.html:182 -#: warehouse/templates/manage/token.html:136 -#: warehouse/templates/manage/token.html:153 +#: warehouse/templates/manage/token.html:138 +#: warehouse/templates/manage/token.html:155 #: warehouse/templates/re-auth.html:51 msgid "(required)" msgstr "" @@ -920,7 +921,7 @@ msgstr "" #: warehouse/templates/accounts/login.html:49 #: warehouse/templates/accounts/profile.html:32 #: warehouse/templates/accounts/register.html:89 -#: warehouse/templates/manage/account.html:272 +#: warehouse/templates/manage/account.html:273 #: warehouse/templates/manage/account/recovery_codes-provision.html:85 #: warehouse/templates/manage/roles.html:173 msgid "Username" @@ -947,7 +948,7 @@ msgid "Profile of %(username)s" msgstr "" #: warehouse/templates/accounts/profile.html:23 -#: warehouse/templates/manage/account.html:247 +#: warehouse/templates/manage/account.html:248 #: warehouse/templates/manage/roles.html:62 #: warehouse/templates/manage/roles.html:125 msgid "Avatar for {user} from gravatar.com" @@ -992,7 +993,7 @@ msgid "%(user)s has not uploaded any projects to PyPI, yet" msgstr "" #: warehouse/templates/accounts/recovery-code.html:18 -#: warehouse/templates/manage/account.html:493 +#: warehouse/templates/manage/account.html:494 msgid "Recovery codes" msgstr "" @@ -1033,8 +1034,8 @@ msgid "Create an account on %(title)s" msgstr "" #: warehouse/templates/accounts/register.html:45 -#: warehouse/templates/manage/account.html:163 -#: warehouse/templates/manage/account.html:529 +#: warehouse/templates/manage/account.html:164 +#: warehouse/templates/manage/account.html:530 msgid "Name" msgstr "" @@ -1043,12 +1044,12 @@ msgid "Your name" msgstr "" #: warehouse/templates/accounts/register.html:64 -#: warehouse/templates/manage/account.html:350 +#: warehouse/templates/manage/account.html:351 msgid "Email address" msgstr "" #: warehouse/templates/accounts/register.html:69 -#: warehouse/templates/manage/account.html:371 +#: warehouse/templates/manage/account.html:372 msgid "Your email address" msgstr "" @@ -1062,7 +1063,7 @@ msgstr "" #: warehouse/templates/accounts/register.html:117 #: warehouse/templates/accounts/reset-password.html:44 -#: warehouse/templates/manage/account.html:398 +#: warehouse/templates/manage/account.html:399 msgid "Show passwords" msgstr "" @@ -1120,7 +1121,7 @@ msgid "Reset your password" msgstr "" #: warehouse/templates/accounts/reset-password.html:47 -#: warehouse/templates/manage/account.html:415 +#: warehouse/templates/manage/account.html:416 msgid "Select a new password" msgstr "" @@ -1431,9 +1432,9 @@ msgstr "" #: warehouse/templates/includes/hash-modal.html:19 #: warehouse/templates/includes/hash-modal.html:68 #: warehouse/templates/includes/session-notifications.html:41 -#: warehouse/templates/manage/account.html:223 -#: warehouse/templates/manage/account.html:225 -#: warehouse/templates/manage/account.html:235 +#: warehouse/templates/manage/account.html:224 +#: warehouse/templates/manage/account.html:226 +#: warehouse/templates/manage/account.html:236 #: warehouse/templates/manage/manage_base.html:94 #: warehouse/templates/manage/manage_base.html:96 #: warehouse/templates/manage/release.html:118 @@ -1471,7 +1472,7 @@ msgstr "" #: warehouse/templates/includes/hash-modal.html:41 #: warehouse/templates/includes/hash-modal.html:50 #: warehouse/templates/includes/hash-modal.html:59 -#: warehouse/templates/manage/account.html:230 +#: warehouse/templates/manage/account.html:231 #: warehouse/templates/manage/account/recovery_codes-provision.html:61 #: warehouse/templates/manage/account/totp-provision.html:57 #: warehouse/templates/packaging/detail.html:117 @@ -1482,7 +1483,7 @@ msgstr "" #: warehouse/templates/includes/hash-modal.html:42 #: warehouse/templates/includes/hash-modal.html:51 #: warehouse/templates/includes/hash-modal.html:60 -#: warehouse/templates/manage/account.html:231 +#: warehouse/templates/manage/account.html:232 #: warehouse/templates/manage/account/recovery_codes-provision.html:62 #: warehouse/templates/manage/account/totp-provision.html:58 #: warehouse/templates/pages/classifiers.html:38 @@ -1586,7 +1587,7 @@ msgid "Collaborators" msgstr "" #: warehouse/templates/includes/manage/manage-project-menu.html:31 -#: warehouse/templates/manage/account.html:554 +#: warehouse/templates/manage/account.html:555 #: warehouse/templates/manage/history.html:23 msgid "Security history" msgstr "" @@ -1718,7 +1719,7 @@ msgid "View email options" msgstr "" #: warehouse/templates/manage/account.html:87 -#: warehouse/templates/manage/account.html:187 +#: warehouse/templates/manage/account.html:188 #: warehouse/templates/manage/release.html:67 #: warehouse/templates/manage/releases.html:68 msgid "Options" @@ -1782,67 +1783,67 @@ msgid "" "authentication with a security device (e.g. USB key)" msgstr "" -#: warehouse/templates/manage/account.html:167 -#: warehouse/templates/manage/account.html:530 -#: warehouse/templates/manage/token.html:151 +#: warehouse/templates/manage/account.html:168 +#: warehouse/templates/manage/account.html:531 +#: warehouse/templates/manage/token.html:153 msgid "Scope" msgstr "" -#: warehouse/templates/manage/account.html:169 +#: warehouse/templates/manage/account.html:170 msgid "All projects" msgstr "" -#: warehouse/templates/manage/account.html:177 -#: warehouse/templates/manage/account.html:531 +#: warehouse/templates/manage/account.html:178 +#: warehouse/templates/manage/account.html:532 msgid "Created" msgstr "" -#: warehouse/templates/manage/account.html:181 -#: warehouse/templates/manage/account.html:532 +#: warehouse/templates/manage/account.html:182 +#: warehouse/templates/manage/account.html:533 msgid "Last used" msgstr "" -#: warehouse/templates/manage/account.html:182 +#: warehouse/templates/manage/account.html:183 msgid "Never" msgstr "" -#: warehouse/templates/manage/account.html:186 +#: warehouse/templates/manage/account.html:187 msgid "View token options" msgstr "" -#: warehouse/templates/manage/account.html:196 -#: warehouse/templates/manage/token.html:57 +#: warehouse/templates/manage/account.html:197 +#: warehouse/templates/manage/token.html:59 msgid "Remove token" msgstr "" -#: warehouse/templates/manage/account.html:202 +#: warehouse/templates/manage/account.html:203 msgid "View unique identifier" msgstr "" -#: warehouse/templates/manage/account.html:210 -#: warehouse/templates/manage/account.html:212 -#: warehouse/templates/manage/token.html:60 -#: warehouse/templates/manage/token.html:61 +#: warehouse/templates/manage/account.html:211 +#: warehouse/templates/manage/account.html:213 +#: warehouse/templates/manage/token.html:62 +#: warehouse/templates/manage/token.html:63 msgid "Remove API token" msgstr "" -#: warehouse/templates/manage/account.html:217 -#: warehouse/templates/manage/token.html:66 +#: warehouse/templates/manage/account.html:218 +#: warehouse/templates/manage/token.html:68 msgid "" "Applications or scripts using this token will no longer have access to " "PyPI." msgstr "" -#: warehouse/templates/manage/account.html:228 +#: warehouse/templates/manage/account.html:229 #, python-format msgid "Unique identifier for API token \"%(token_description)s\"" msgstr "" -#: warehouse/templates/manage/account.html:246 +#: warehouse/templates/manage/account.html:247 msgid "Profile picture" msgstr "" -#: warehouse/templates/manage/account.html:252 +#: warehouse/templates/manage/account.html:253 #, python-format msgid "" "We use public profile. Cannot be " "changed." msgstr "" -#: warehouse/templates/manage/account.html:292 +#: warehouse/templates/manage/account.html:293 msgid "Full name" msgstr "" -#: warehouse/templates/manage/account.html:297 +#: warehouse/templates/manage/account.html:298 msgid "No name set" msgstr "" -#: warehouse/templates/manage/account.html:302 +#: warehouse/templates/manage/account.html:303 #, python-format msgid "Displayed on your public profile" msgstr "" -#: warehouse/templates/manage/account.html:309 +#: warehouse/templates/manage/account.html:310 msgid "️Public email" msgstr "" -#: warehouse/templates/manage/account.html:321 +#: warehouse/templates/manage/account.html:322 #, python-format msgid "" "One of your verified emails can be displayed on your public profile to logged-in users." msgstr "" -#: warehouse/templates/manage/account.html:326 +#: warehouse/templates/manage/account.html:327 msgid "Update account" msgstr "" -#: warehouse/templates/manage/account.html:334 +#: warehouse/templates/manage/account.html:335 msgid "Account emails" msgstr "" -#: warehouse/templates/manage/account.html:336 +#: warehouse/templates/manage/account.html:337 msgid "" "You can associate several emails with your account. You can use any 2FA." msgstr "" -#: warehouse/templates/manage/account.html:453 +#: warehouse/templates/manage/account.html:454 msgid "Two factor authentication methods enabled" msgstr "" -#: warehouse/templates/manage/account.html:456 +#: warehouse/templates/manage/account.html:457 msgid "Two factor method" msgstr "" -#: warehouse/templates/manage/account.html:462 -#: warehouse/templates/manage/account.html:572 +#: warehouse/templates/manage/account.html:463 +#: warehouse/templates/manage/account.html:573 msgid "" "Authentication application (TOTP)" msgstr "" -#: warehouse/templates/manage/account.html:464 -#: warehouse/templates/manage/account.html:478 +#: warehouse/templates/manage/account.html:465 +#: warehouse/templates/manage/account.html:479 msgid "Remove" msgstr "" -#: warehouse/templates/manage/account.html:465 +#: warehouse/templates/manage/account.html:466 msgid "Remove authentication application" msgstr "" -#: warehouse/templates/manage/account.html:466 +#: warehouse/templates/manage/account.html:467 msgid "Remove application" msgstr "" -#: warehouse/templates/manage/account.html:475 -#: warehouse/templates/manage/account.html:570 +#: warehouse/templates/manage/account.html:476 +#: warehouse/templates/manage/account.html:571 msgid "Security device (WebAuthn)" msgstr "" -#: warehouse/templates/manage/account.html:479 +#: warehouse/templates/manage/account.html:480 msgid "Remove two factor security device" msgstr "" -#: warehouse/templates/manage/account.html:480 +#: warehouse/templates/manage/account.html:481 msgid "Remove device" msgstr "" -#: warehouse/templates/manage/account.html:486 +#: warehouse/templates/manage/account.html:487 msgid "Device name" msgstr "" -#: warehouse/templates/manage/account.html:493 +#: warehouse/templates/manage/account.html:494 #, python-format msgid "generated %(generated_datetime)s" msgstr "" -#: warehouse/templates/manage/account.html:496 +#: warehouse/templates/manage/account.html:497 msgid "Regenerate" msgstr "" -#: warehouse/templates/manage/account.html:497 #: warehouse/templates/manage/account.html:498 +#: warehouse/templates/manage/account.html:499 #: warehouse/templates/manage/account/recovery_codes-provision.html:36 #: warehouse/templates/manage/account/recovery_codes-provision.html:37 #: warehouse/templates/manage/account/recovery_codes-provision.html:38 msgid "Regenerate recovery codes" msgstr "" -#: warehouse/templates/manage/account.html:509 +#: warehouse/templates/manage/account.html:510 msgid "You have not enabled two factor authentication on your account." msgstr "" -#: warehouse/templates/manage/account.html:514 +#: warehouse/templates/manage/account.html:515 #, python-format msgid "" "Verify your primary email address to add two " "factor authentication to your account." msgstr "" -#: warehouse/templates/manage/account.html:521 +#: warehouse/templates/manage/account.html:522 #: warehouse/templates/manage/settings.html:43 msgid "API tokens" msgstr "" -#: warehouse/templates/manage/account.html:522 +#: warehouse/templates/manage/account.html:523 #: warehouse/templates/manage/settings.html:44 msgid "" "API tokens provide an alternative way to authenticate when uploading " "packages to PyPI." msgstr "" -#: warehouse/templates/manage/account.html:522 +#: warehouse/templates/manage/account.html:523 msgid "Learn more about API tokens" msgstr "" -#: warehouse/templates/manage/account.html:526 +#: warehouse/templates/manage/account.html:527 msgid "Active API tokens for this account" msgstr "" -#: warehouse/templates/manage/account.html:544 +#: warehouse/templates/manage/account.html:545 #: warehouse/templates/manage/token.html:17 msgid "Add API token" msgstr "" -#: warehouse/templates/manage/account.html:546 +#: warehouse/templates/manage/account.html:547 #, python-format msgid "" "Verify your primary email address to add API " "tokens to your account." msgstr "" -#: warehouse/templates/manage/account.html:561 +#: warehouse/templates/manage/account.html:562 msgid "Account created" msgstr "" -#: warehouse/templates/manage/account.html:564 +#: warehouse/templates/manage/account.html:565 msgid "Logged in" msgstr "" -#: warehouse/templates/manage/account.html:566 +#: warehouse/templates/manage/account.html:567 msgid "Two factor method:" msgstr "" -#: warehouse/templates/manage/account.html:568 +#: warehouse/templates/manage/account.html:569 #: warehouse/templates/manage/release.html:58 #: warehouse/templates/packaging/detail.html:350 msgid "None" msgstr "" -#: warehouse/templates/manage/account.html:574 +#: warehouse/templates/manage/account.html:575 msgid "Recovery code" msgstr "" -#: warehouse/templates/manage/account.html:579 +#: warehouse/templates/manage/account.html:580 msgid "Login failed" msgstr "" -#: warehouse/templates/manage/account.html:582 +#: warehouse/templates/manage/account.html:583 msgid "- Basic Auth (Upload endpoint)" msgstr "" -#: warehouse/templates/manage/account.html:587 -#: warehouse/templates/manage/account.html:604 +#: warehouse/templates/manage/account.html:588 +#: warehouse/templates/manage/account.html:605 #: warehouse/templates/manage/history.html:84 msgid "Reason:" msgstr "" -#: warehouse/templates/manage/account.html:589 -#: warehouse/templates/manage/account.html:606 +#: warehouse/templates/manage/account.html:590 +#: warehouse/templates/manage/account.html:607 msgid "Incorrect Password" msgstr "" -#: warehouse/templates/manage/account.html:591 +#: warehouse/templates/manage/account.html:592 msgid "Invalid two factor (TOTP)" msgstr "" -#: warehouse/templates/manage/account.html:593 +#: warehouse/templates/manage/account.html:594 msgid "Invalid two factor (WebAuthn)" msgstr "" -#: warehouse/templates/manage/account.html:595 +#: warehouse/templates/manage/account.html:596 msgid "Invalid two factor (Recovery code)" msgstr "" -#: warehouse/templates/manage/account.html:602 +#: warehouse/templates/manage/account.html:603 msgid "Session reauthentication failed" msgstr "" -#: warehouse/templates/manage/account.html:613 +#: warehouse/templates/manage/account.html:614 msgid "Email added to account" msgstr "" -#: warehouse/templates/manage/account.html:616 +#: warehouse/templates/manage/account.html:617 msgid "Email removed from account" msgstr "" -#: warehouse/templates/manage/account.html:619 +#: warehouse/templates/manage/account.html:620 msgid "Email verified" msgstr "" -#: warehouse/templates/manage/account.html:622 +#: warehouse/templates/manage/account.html:623 msgid "Email reverified" msgstr "" -#: warehouse/templates/manage/account.html:626 +#: warehouse/templates/manage/account.html:627 msgid "Primary email changed" msgstr "" -#: warehouse/templates/manage/account.html:628 +#: warehouse/templates/manage/account.html:629 msgid "Old primary email:" msgstr "" -#: warehouse/templates/manage/account.html:629 +#: warehouse/templates/manage/account.html:630 msgid "New primary email:" msgstr "" -#: warehouse/templates/manage/account.html:632 +#: warehouse/templates/manage/account.html:633 msgid "Primary email set" msgstr "" -#: warehouse/templates/manage/account.html:638 +#: warehouse/templates/manage/account.html:639 msgid "Email sent" msgstr "" -#: warehouse/templates/manage/account.html:640 +#: warehouse/templates/manage/account.html:641 msgid "From:" msgstr "" -#: warehouse/templates/manage/account.html:641 +#: warehouse/templates/manage/account.html:642 msgid "To:" msgstr "" -#: warehouse/templates/manage/account.html:642 +#: warehouse/templates/manage/account.html:643 msgid "Subject:" msgstr "" -#: warehouse/templates/manage/account.html:646 +#: warehouse/templates/manage/account.html:647 msgid "Password reset requested" msgstr "" -#: warehouse/templates/manage/account.html:648 +#: warehouse/templates/manage/account.html:649 msgid "Password reset attempted" msgstr "" -#: warehouse/templates/manage/account.html:650 +#: warehouse/templates/manage/account.html:651 msgid "Password successfully reset" msgstr "" -#: warehouse/templates/manage/account.html:652 +#: warehouse/templates/manage/account.html:653 msgid "Password successfully changed" msgstr "" -#: warehouse/templates/manage/account.html:655 +#: warehouse/templates/manage/account.html:656 msgid "Two factor authentication added" msgstr "" -#: warehouse/templates/manage/account.html:658 -#: warehouse/templates/manage/account.html:668 +#: warehouse/templates/manage/account.html:659 +#: warehouse/templates/manage/account.html:669 msgid "" "Method: Security device (WebAuthn)" msgstr "" -#: warehouse/templates/manage/account.html:659 -#: warehouse/templates/manage/account.html:669 +#: warehouse/templates/manage/account.html:660 +#: warehouse/templates/manage/account.html:670 msgid "Device name:" msgstr "" -#: warehouse/templates/manage/account.html:661 -#: warehouse/templates/manage/account.html:671 +#: warehouse/templates/manage/account.html:662 +#: warehouse/templates/manage/account.html:672 msgid "" "Method: Authentication application (TOTP)" msgstr "" -#: warehouse/templates/manage/account.html:665 +#: warehouse/templates/manage/account.html:666 msgid "Two factor authentication removed" msgstr "" -#: warehouse/templates/manage/account.html:676 +#: warehouse/templates/manage/account.html:677 msgid "Recovery codes generated" msgstr "" -#: warehouse/templates/manage/account.html:680 +#: warehouse/templates/manage/account.html:681 msgid "Recovery codes regenerated" msgstr "" -#: warehouse/templates/manage/account.html:684 +#: warehouse/templates/manage/account.html:685 msgid "Recovery code used for login" msgstr "" -#: warehouse/templates/manage/account.html:690 +#: warehouse/templates/manage/account.html:692 #: warehouse/templates/manage/history.html:65 msgid "API token added" msgstr "" -#: warehouse/templates/manage/account.html:692 -#: warehouse/templates/manage/account.html:707 +#: warehouse/templates/manage/account.html:694 +#: warehouse/templates/manage/account.html:710 #: warehouse/templates/manage/history.html:69 #: warehouse/templates/manage/history.html:76 msgid "Token name:" msgstr "" -#: warehouse/templates/manage/account.html:694 -#: warehouse/templates/manage/account.html:710 +#: warehouse/templates/manage/account.html:696 +#: warehouse/templates/manage/account.html:713 msgid "Token scope: entire account" msgstr "" -#: warehouse/templates/manage/account.html:696 -#: warehouse/templates/manage/account.html:712 +#: warehouse/templates/manage/account.html:698 +#: warehouse/templates/manage/account.html:715 #, python-format msgid "Token scope: Project %(project_name)s" msgstr "" -#: warehouse/templates/manage/account.html:701 +#: warehouse/templates/manage/account.html:703 #: warehouse/templates/manage/history.html:72 msgid "API token removed" msgstr "" -#: warehouse/templates/manage/account.html:702 -#: warehouse/templates/manage/account.html:708 +#: warehouse/templates/manage/account.html:704 +#: warehouse/templates/manage/account.html:711 msgid "Unique identifier:" msgstr "" -#: warehouse/templates/manage/account.html:705 +#: warehouse/templates/manage/account.html:708 msgid "API token automatically removed for security reasons" msgstr "" -#: warehouse/templates/manage/account.html:714 +#: warehouse/templates/manage/account.html:717 #, python-format msgid "Reason: Token found at public url" msgstr "" -#: warehouse/templates/manage/account.html:723 +#: warehouse/templates/manage/account.html:726 #, python-format msgid "" "Events appear here as security-related actions occur on your account. If " @@ -2274,42 +2275,42 @@ msgid "" "your account as soon as possible." msgstr "" -#: warehouse/templates/manage/account.html:728 +#: warehouse/templates/manage/account.html:731 msgid "Recent account activity" msgstr "" -#: warehouse/templates/manage/account.html:730 +#: warehouse/templates/manage/account.html:733 #: warehouse/templates/manage/history.html:97 msgid "Event" msgstr "" -#: warehouse/templates/manage/account.html:731 -#: warehouse/templates/manage/account.html:739 +#: warehouse/templates/manage/account.html:734 +#: warehouse/templates/manage/account.html:742 #: warehouse/templates/manage/history.html:98 #: warehouse/templates/manage/history.html:107 msgid "Date / time" msgstr "" -#: warehouse/templates/manage/account.html:732 -#: warehouse/templates/manage/account.html:743 +#: warehouse/templates/manage/account.html:735 +#: warehouse/templates/manage/account.html:746 #: warehouse/templates/manage/history.html:99 #: warehouse/templates/manage/history.html:111 msgid "IP address" msgstr "" -#: warehouse/templates/manage/account.html:751 +#: warehouse/templates/manage/account.html:754 msgid "Events will appear here as security-related actions occur on your account." msgstr "" -#: warehouse/templates/manage/account.html:758 +#: warehouse/templates/manage/account.html:761 msgid "Delete account" msgstr "" -#: warehouse/templates/manage/account.html:761 +#: warehouse/templates/manage/account.html:764 msgid "Cannot delete account" msgstr "" -#: warehouse/templates/manage/account.html:763 +#: warehouse/templates/manage/account.html:766 #, python-format msgid "" "\n" @@ -2324,7 +2325,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/manage/account.html:768 +#: warehouse/templates/manage/account.html:771 msgid "" "\n" " You must transfer ownership or delete this project before you " @@ -2338,23 +2339,23 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/manage/account.html:778 +#: warehouse/templates/manage/account.html:781 #, python-format msgid "" "transfer ownership or delete project" msgstr "" -#: warehouse/templates/manage/account.html:787 -#: warehouse/templates/manage/token.html:169 +#: warehouse/templates/manage/account.html:790 +#: warehouse/templates/manage/token.html:171 msgid "Proceed with caution!" msgstr "" -#: warehouse/templates/manage/account.html:790 +#: warehouse/templates/manage/account.html:793 msgid "You will not be able to recover your account after you delete it" msgstr "" -#: warehouse/templates/manage/account.html:792 +#: warehouse/templates/manage/account.html:795 msgid "Delete your PyPI account" msgstr "" @@ -3188,70 +3189,69 @@ msgstr "" msgid "Project Name" msgstr "" -#: warehouse/templates/manage/token.html:38 +#: warehouse/templates/manage/token.html:39 #, python-format msgid "Token for \"%(macaroon_description)s\"" msgstr "" -#: warehouse/templates/manage/token.html:40 +#: warehouse/templates/manage/token.html:41 msgid "Permissions:" msgstr "" -#: warehouse/templates/manage/token.html:40 -#: warehouse/templates/manage/token.html:147 +#: warehouse/templates/manage/token.html:41 +#: warehouse/templates/manage/token.html:149 msgid "Upload packages" msgstr "" #: warehouse/templates/manage/token.html:42 -#: warehouse/templates/manage/token.html:44 msgid "Scope:" msgstr "" -#: warehouse/templates/manage/token.html:42 -#: warehouse/templates/manage/token.html:158 +#: warehouse/templates/manage/token.html:44 +#: warehouse/templates/manage/token.html:160 msgid "Entire account (all projects)" msgstr "" -#: warehouse/templates/manage/token.html:44 +#: warehouse/templates/manage/token.html:46 #, python-format msgid "Project \"%(project)s\"" msgstr "" -#: warehouse/templates/manage/token.html:51 +#: warehouse/templates/manage/token.html:53 msgid "" "For security reasons this token will only appear once. Copy it " "now." msgstr "" -#: warehouse/templates/manage/token.html:53 +#: warehouse/templates/manage/token.html:55 msgid "Copy token to clipboard" msgstr "" -#: warehouse/templates/manage/token.html:54 +#: warehouse/templates/manage/token.html:56 msgid "Copy token" msgstr "" -#: warehouse/templates/manage/token.html:72 +#: warehouse/templates/manage/token.html:74 msgid "Using this token" msgstr "" -#: warehouse/templates/manage/token.html:74 +#: warehouse/templates/manage/token.html:76 msgid "To use this API token:" msgstr "" -#: warehouse/templates/manage/token.html:77 +#: warehouse/templates/manage/token.html:79 #, python-format msgid "Set your username to %(token)s" msgstr "" -#: warehouse/templates/manage/token.html:78 +#: warehouse/templates/manage/token.html:80 #, python-format msgid "" "Set your password to the token value, including the " "%(prefix)s prefix" msgstr "" -#: warehouse/templates/manage/token.html:84 +#: warehouse/templates/manage/token.html:86 #, python-format msgid "" "For example, if you are using Twine to upload " @@ -3259,7 +3259,7 @@ msgid "" "this:" msgstr "" -#: warehouse/templates/manage/token.html:94 +#: warehouse/templates/manage/token.html:96 #, python-format msgid "" "For example, if you are using Twine to upload " @@ -3267,61 +3267,61 @@ msgid "" "file like this:" msgstr "" -#: warehouse/templates/manage/token.html:106 +#: warehouse/templates/manage/token.html:108 msgid "" "either a user-scoped token or a project-scoped token you want to set as " "the default" msgstr "" -#: warehouse/templates/manage/token.html:111 +#: warehouse/templates/manage/token.html:113 msgid "a project token" msgstr "" -#: warehouse/templates/manage/token.html:113 +#: warehouse/templates/manage/token.html:115 #, python-format msgid "" "You can then use %(command)s to switch to the correct token " "when uploading to PyPI." msgstr "" -#: warehouse/templates/manage/token.html:119 +#: warehouse/templates/manage/token.html:121 #, python-format msgid "" "For further instructions on how to use this token, visit the PyPI help page." msgstr "" -#: warehouse/templates/manage/token.html:127 +#: warehouse/templates/manage/token.html:129 msgid "Add another token" msgstr "" -#: warehouse/templates/manage/token.html:134 +#: warehouse/templates/manage/token.html:136 msgid "Token name" msgstr "" -#: warehouse/templates/manage/token.html:143 +#: warehouse/templates/manage/token.html:145 msgid "What is this token for?" msgstr "" -#: warehouse/templates/manage/token.html:146 +#: warehouse/templates/manage/token.html:148 msgid "Permissions" msgstr "" -#: warehouse/templates/manage/token.html:157 +#: warehouse/templates/manage/token.html:159 msgid "Select scope..." msgstr "" -#: warehouse/templates/manage/token.html:161 +#: warehouse/templates/manage/token.html:163 msgid "Project:" msgstr "" -#: warehouse/templates/manage/token.html:170 +#: warehouse/templates/manage/token.html:172 msgid "" "An API token scoped to your entire account will have upload permissions " "for all of your current and future projects." msgstr "" -#: warehouse/templates/manage/token.html:173 +#: warehouse/templates/manage/token.html:175 msgid "Add token" msgstr "" @@ -4450,10 +4450,15 @@ msgid "" msgstr "" #: warehouse/templates/pages/help.html:502 +#, python-format msgid "" -"Advanced users may wish to inspect their token by decoding it with " -"base64, and checking the output against the unique identifier displayed " -"on PyPI." +"Advanced users may wish to inspect their token by decoding it with the pypitoken library, and checking the" +" identifier against the unique identifier displayed on PyPI. Please make " +"sure to keep your token identifier private, though. Knowing just the " +"identifier of a token is not enough to impersonate the user, but it's " +"sufficient to have a token be disabled." msgstr "" #: warehouse/templates/pages/help.html:506