Repository: pypi/warehouse

Issue:
API token default scope: user or project?
Followup to #994: 

@brettcannon [asks](https://discuss.python.org/t/pypi-security-work-multifactor-auth-progress-help-needed/1042/33):

> > @ewdurbin said: By default, newly created tokens will have “user” scope, meaning that they’ll behave exactly like your password.

> Are there plans to change this default so that using such a strong token is not the default so that people have to opt into it? (I’m no security expert so this is more inquisitive.)

As far as I know there are no such plans but I'd like @woodruffw and @nlhkabu to weigh in.


Discussion:
An idea: We could add some additional UI on creation of a user-scoped token, warning the user that their new token will have access to all of their projects. This would allow us to retain it as a default (which I think is sensible, at least insofar as it doesn't make sense to choose a random project from the user's list as a default) while also making the security properties clear.

Resolving patch:
diff --git a/warehouse/manage/forms.py b/warehouse/manage/forms.py
--- a/warehouse/manage/forms.py
+++ b/warehouse/manage/forms.py
@@ -196,7 +196,7 @@ def __init__(self, *args, user_id, macaroon_service, project_names, **kwargs):
 
     description = wtforms.StringField(
         validators=[
-            wtforms.validators.DataRequired(message="Specify a description"),
+            wtforms.validators.DataRequired(message="Specify a token name"),
             wtforms.validators.Length(
                 max=100, message="Description must be 100 characters or less"
             ),
@@ -204,7 +204,7 @@ def __init__(self, *args, user_id, macaroon_service, project_names, **kwargs):
     )
 
     token_scope = wtforms.StringField(
-        validators=[wtforms.validators.DataRequired(message="Specify a token scope")]
+        validators=[wtforms.validators.DataRequired(message="Specify the token scope")]
     )
 
     def validate_description(self, field):
@@ -224,6 +224,9 @@ def validate_token_scope(self, field):
         except ValueError:
             raise wtforms.ValidationError(f"Unknown token scope: {scope}")
 
+        if scope_kind == "unspecified":
+            raise wtforms.ValidationError(f"Specify the token scope")
+
         if scope_kind == "user":
             self.validated_scope = scope_kind
             return


Test patch:
diff --git a/tests/unit/manage/test_forms.py b/tests/unit/manage/test_forms.py
--- a/tests/unit/manage/test_forms.py
+++ b/tests/unit/manage/test_forms.py
@@ -347,7 +347,7 @@ def test_validate_description_missing(self):
         )
 
         assert not form.validate()
-        assert form.description.errors.pop() == "Specify a description"
+        assert form.description.errors.pop() == "Specify a token name"
 
     def test_validate_description_in_use(self):
         form = forms.CreateMacaroonForm(
@@ -371,7 +371,18 @@ def test_validate_token_scope_missing(self):
         )
 
         assert not form.validate()
-        assert form.token_scope.errors.pop() == "Specify a token scope"
+        assert form.token_scope.errors.pop() == "Specify the token scope"
+
+    def test_validate_token_scope_unspecified(self):
+        form = forms.CreateMacaroonForm(
+            data={"description": "dummy", "token_scope": "scope:unspecified"},
+            user_id=pretend.stub(),
+            macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
+            project_names=pretend.stub(),
+        )
+
+        assert not form.validate()
+        assert form.token_scope.errors.pop() == "Specify the token scope"
 
     @pytest.mark.parametrize(
         ("scope"), ["not a real scope", "scope:project", "scope:foo:bar"]