Skip to content

Commit 8a8332d

Browse files
author
Luka Sterbic
committed
Persist create token form params on error
1 parent 77f91ca commit 8a8332d

File tree

6 files changed

+99
-48
lines changed

6 files changed

+99
-48
lines changed

tests/unit/manage/test_forms.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ def test_creation(self):
340340

341341
def test_validate_description_missing(self):
342342
form = forms.CreateMacaroonForm(
343-
data={"token_scope": "scope:user"},
343+
data={"token_scopes": ["scope:user"]},
344344
user_id=pretend.stub(),
345345
macaroon_service=pretend.stub(),
346346
project_names=pretend.stub(),
@@ -351,7 +351,7 @@ def test_validate_description_missing(self):
351351

352352
def test_validate_description_in_use(self):
353353
form = forms.CreateMacaroonForm(
354-
data={"description": "dummy", "token_scope": "scope:user"},
354+
data={"description": "dummy", "token_scopes": ["scope:user"]},
355355
user_id=pretend.stub(),
356356
macaroon_service=pretend.stub(
357357
get_macaroon_by_description=lambda *a: pretend.stub()
@@ -371,47 +371,61 @@ def test_validate_token_scope_missing(self):
371371
)
372372

373373
assert not form.validate()
374-
assert form.token_scope.errors.pop() == "Specify the token scope"
374+
assert form.token_scopes.errors.pop() == "Specify the token scope"
375375

376376
def test_validate_token_scope_unspecified(self):
377377
form = forms.CreateMacaroonForm(
378-
data={"description": "dummy", "token_scope": "scope:unspecified"},
378+
data={"description": "dummy", "token_scopes": ["scope:unspecified"]},
379379
user_id=pretend.stub(),
380380
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
381381
project_names=pretend.stub(),
382382
)
383383

384384
assert not form.validate()
385-
assert form.token_scope.errors.pop() == "Specify the token scope"
385+
assert form.token_scopes.errors.pop() == "Specify the token scope"
386386

387387
@pytest.mark.parametrize(
388388
("scope"), ["not a real scope", "scope:project", "scope:foo:bar"]
389389
)
390390
def test_validate_token_scope_invalid_format(self, scope):
391391
form = forms.CreateMacaroonForm(
392-
data={"description": "dummy", "token_scope": scope},
392+
data={"description": "dummy", "token_scopes": [scope]},
393393
user_id=pretend.stub(),
394394
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
395395
project_names=pretend.stub(),
396396
)
397397

398398
assert not form.validate()
399-
assert form.token_scope.errors.pop() == f"Unknown token scope: {scope}"
399+
assert form.token_scopes.errors.pop() == f"Unknown token scope: {scope}"
400400

401401
def test_validate_token_scope_invalid_project(self):
402402
form = forms.CreateMacaroonForm(
403-
data={"description": "dummy", "token_scope": "scope:project:foo"},
403+
data={"description": "dummy", "token_scopes": ["scope:project:foo"]},
404404
user_id=pretend.stub(),
405405
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
406406
project_names=["bar"],
407407
)
408408

409409
assert not form.validate()
410-
assert form.token_scope.errors.pop() == "Unknown or invalid project name: foo"
410+
assert form.token_scopes.errors.pop() == "Unknown or invalid project name: foo"
411+
412+
def test_validate_token_scope_user_and_project(self):
413+
form = forms.CreateMacaroonForm(
414+
data={
415+
"description": "dummy",
416+
"token_scopes": ["scope:project:bar", "scope:user"],
417+
},
418+
user_id=pretend.stub(),
419+
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
420+
project_names=["bar"],
421+
)
422+
423+
assert not form.validate()
424+
assert form.token_scopes.errors.pop() == "Mixed user and project scopes"
411425

412426
def test_validate_token_scope_valid_user(self):
413427
form = forms.CreateMacaroonForm(
414-
data={"description": "dummy", "token_scope": "scope:user"},
428+
data={"description": "dummy", "token_scopes": ["scope:user"]},
415429
user_id=pretend.stub(),
416430
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
417431
project_names=pretend.stub(),
@@ -421,14 +435,27 @@ def test_validate_token_scope_valid_user(self):
421435

422436
def test_validate_token_scope_valid_project(self):
423437
form = forms.CreateMacaroonForm(
424-
data={"description": "dummy", "token_scope": "scope:project:foo"},
438+
data={"description": "dummy", "token_scopes": ["scope:project:foo"]},
425439
user_id=pretend.stub(),
426440
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
427441
project_names=["foo"],
428442
)
429443

430444
assert form.validate()
431445

446+
def test_validate_token_scope_valid_projects(self):
447+
form = forms.CreateMacaroonForm(
448+
data={
449+
"description": "dummy",
450+
"token_scopes": ["scope:project:foo", "scope:project:bar"],
451+
},
452+
user_id=pretend.stub(),
453+
macaroon_service=pretend.stub(get_macaroon_by_description=lambda *a: None),
454+
project_names=["foo", "bar"],
455+
)
456+
457+
assert form.validate()
458+
432459

433460
class TestDeleteMacaroonForm:
434461
def test_creation(self):

tests/unit/manage/test_views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,7 +1427,7 @@ def test_create_macaroon_invalid_form(self, monkeypatch):
14271427
create_macaroon=pretend.call_recorder(lambda *a, **kw: pretend.stub())
14281428
)
14291429
request = pretend.stub(
1430-
POST={},
1430+
POST=MultiDict({"description": "dummy"}),
14311431
user=pretend.stub(id=pretend.stub(), has_primary_verified_email=True),
14321432
find_service=lambda interface, **kw: {
14331433
IMacaroonService: macaroon_service,
@@ -1468,7 +1468,7 @@ def test_create_macaroon(self, monkeypatch):
14681468
)
14691469
)
14701470
request = pretend.stub(
1471-
POST={},
1471+
POST=MultiDict({"description": "dummy"}),
14721472
domain=pretend.stub(),
14731473
user=pretend.stub(id=pretend.stub(), has_primary_verified_email=True),
14741474
find_service=lambda interface, **kw: {
@@ -1523,7 +1523,7 @@ def test_delete_macaroon_invalid_form(self, monkeypatch):
15231523
delete_macaroon=pretend.call_recorder(lambda id: pretend.stub())
15241524
)
15251525
request = pretend.stub(
1526-
POST={},
1526+
POST=MultiDict({"description": "dummy"}),
15271527
route_path=pretend.call_recorder(lambda x: pretend.stub()),
15281528
find_service=lambda interface, **kw: {
15291529
IMacaroonService: macaroon_service,

warehouse/manage/forms.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def validate_label(self, field):
186186

187187

188188
class CreateMacaroonForm(forms.Form):
189-
__params__ = ["description", "token_scope"]
189+
__params__ = ["description", "token_scopes"]
190190

191191
def __init__(self, *args, user_id, macaroon_service, project_names, **kwargs):
192192
super().__init__(*args, **kwargs)
@@ -203,8 +203,13 @@ def __init__(self, *args, user_id, macaroon_service, project_names, **kwargs):
203203
]
204204
)
205205

206-
token_scope = wtforms.StringField(
207-
validators=[wtforms.validators.DataRequired(message="Specify the token scope")]
206+
token_scopes = wtforms.FieldList(
207+
wtforms.StringField(
208+
"scope",
209+
validators=[
210+
wtforms.validators.DataRequired(message="Specify the token scope")
211+
],
212+
)
208213
)
209214

210215
def validate_description(self, field):
@@ -216,34 +221,41 @@ def validate_description(self, field):
216221
):
217222
raise wtforms.validators.ValidationError("API token name already in use")
218223

219-
def validate_token_scope(self, field):
220-
scope = field.data
224+
def validate_token_scopes(self, field):
225+
scopes = field.data
226+
if not scopes:
227+
raise wtforms.ValidationError(f"Specify the token scope")
221228

222-
try:
223-
_, scope_kind = scope.split(":", 1)
224-
except ValueError:
225-
raise wtforms.ValidationError(f"Unknown token scope: {scope}")
229+
self.validated_scope = {"projects": []}
226230

227-
if scope_kind == "unspecified":
228-
raise wtforms.ValidationError(f"Specify the token scope")
231+
for scope in scopes:
232+
try:
233+
_, scope_kind = scope.split(":", 1)
234+
except ValueError:
235+
raise wtforms.ValidationError(f"Unknown token scope: {scope}")
229236

230-
if scope_kind == "user":
231-
self.validated_scope = scope_kind
232-
return
237+
if scope_kind == "unspecified":
238+
raise wtforms.ValidationError(f"Specify the token scope")
233239

234-
try:
235-
scope_kind, scope_value = scope_kind.split(":", 1)
236-
except ValueError:
237-
raise wtforms.ValidationError(f"Unknown token scope: {scope}")
238-
239-
if scope_kind != "project":
240-
raise wtforms.ValidationError(f"Unknown token scope: {scope}")
241-
if scope_value not in self.project_names:
242-
raise wtforms.ValidationError(
243-
f"Unknown or invalid project name: {scope_value}"
244-
)
240+
if scope_kind == "user":
241+
if len(scopes) != 1:
242+
raise wtforms.ValidationError(f"Mixed user and project scopes")
243+
self.validated_scope = scope_kind
244+
return
245+
246+
try:
247+
scope_kind, scope_value = scope_kind.split(":", 1)
248+
except ValueError:
249+
raise wtforms.ValidationError(f"Unknown token scope: {scope}")
250+
251+
if scope_kind != "project":
252+
raise wtforms.ValidationError(f"Unknown token scope: {scope}")
253+
if scope_value not in self.project_names:
254+
raise wtforms.ValidationError(
255+
f"Unknown or invalid project name: {scope_value}"
256+
)
245257

246-
self.validated_scope = {"projects": [scope_value]}
258+
self.validated_scope["projects"].append(scope_value)
247259

248260

249261
class DeleteMacaroonForm(forms.Form):

warehouse/manage/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,8 @@ def create_macaroon(self):
585585
return HTTPSeeOther(self.request.route_path("manage.account"))
586586

587587
form = CreateMacaroonForm(
588-
**self.request.POST,
588+
description=self.request.POST.getone("description"),
589+
token_scopes=self.request.POST.getall("token_scopes"),
589590
user_id=self.request.user.id,
590591
macaroon_service=self.macaroon_service,
591592
project_names=self.project_names,

warehouse/static/sass/blocks/_form-group.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
max-width: 100%;
4545
}
4646

47+
&__multiselect {
48+
height: 200px;
49+
overflow: auto;
50+
}
51+
4752
&__text {
4853
font-size: 20px;
4954
padding: 4px 0 8px;

warehouse/templates/manage/token.html

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ <h2>Token for "{{ macaroon.description }}"</h2>
3535
{% if macaroon.caveats.permissions == "user" %}
3636
<strong>Scope:</strong> Entire account (all projects)
3737
{% else %}
38-
<strong>Scope:</strong> Project "{{ macaroon.caveats.permissions.projects[0] }}"
38+
{% for project in macaroon.caveats.permissions.projects %}
39+
<strong>Scope:</strong> Project "{{ project }}"<br>
40+
{% endfor %}
3941
{% endif %}
4042
</p>
4143
<p>For instructions on how to use this token, <a href="/help#apitoken">visit the PyPI help page</a>.</p>
@@ -85,16 +87,20 @@ <h2>Add another token</h2>
8587
<p class="form-group__text">Upload packages</p>
8688
</div>
8789
<div class="form-group">
88-
<label for="token_scope" class="form-group__label">Scope</label>
89-
<select name="token_scope" id="token_scope" class="form-group__input" aria-describedby="token_scope-errors">
90-
<option disabled {{ "" if create_macaroon_form.token_scope.data else "selected" }} value="scope:unspecified">Select scope...</option>
91-
<option value="scope:user">Entire account (all projects)</option>
90+
<label for="token_scope" class="form-group__label">
91+
Scope
92+
{% if create_macaroon_form.token_scope.flags.required %}
93+
<span class="form-group__required">(required)</span>
94+
{% endif %}
95+
<select multiple name="token_scopes" id="token_scopes" class="form-group__input form-group__multiselect" aria-describedby="token_scopes-errors">
96+
<option disabled {{ "" if create_macaroon_form.token_scopes.data else "selected" }} value="scope:unspecified">Select scope...</option>
97+
<option {{ "selected" if "scope:user" in create_macaroon_form.token_scopes.data else "" }} value="scope:user">Entire account (all projects)</option>
9298
{% for project in project_names %}
9399
{% set project_value = 'scope:project:' + project %}
94-
<option value="{{ project_value }}" {{ "selected" if project_value == create_macaroon_form.token_scope.data else "" }}>Project: {{ project }}</option>
100+
<option value="{{ project_value }}" {{ "selected" if project_value in create_macaroon_form.token_scopes.data else "" }}>Project: {{ project }}</option>
95101
{% endfor %}
96102
</select>
97-
{{ field_errors(create_macaroon_form.token_scope) }}
103+
{{ field_errors(create_macaroon_form.token_scopes) }}
98104
</div>
99105
<div id="api-token-scope-warning" class="callout-block callout-block--warning" hidden>
100106
<h3 class="callout-block__heading">Proceed with caution!</h3>

0 commit comments

Comments
 (0)