Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2fa disabling ask password -- Take 2 #6527

Merged
merged 35 commits into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
365230b
Extract modal close to its own controller
Aug 10, 2019
62bc578
Add `confirm_password_modal` macro
Aug 10, 2019
2672d1e
Add confirm_password_controller and hook up to confirm_password modal
Aug 10, 2019
7dcc1e7
Have DeleteTOTPForm check user's password
Aug 10, 2019
ebe96dc
Pass username and password to form as expected by PasswordMixin
Aug 10, 2019
8b2c976
Fix tests and set password visibility on connect
Aug 10, 2019
2711af9
Add test for username set as instance variable in form
Aug 10, 2019
b1711d1
Rename form field for consistency
Aug 11, 2019
eb10327
Amend copy on incorrect credentials
Aug 13, 2019
1466ff7
Pass parameters to confirm_password_modal
Aug 13, 2019
e061de3
Add confirm password to DeleteMacaroonForm
Aug 13, 2019
32c82cd
Fix tests
Aug 13, 2019
b32a9d6
Fix linting
Aug 13, 2019
46fc81f
Add UsernameMixin to DeleteMacaroonForm and DeleteTOTPForm
Aug 15, 2019
936bacf
Fix linting
Aug 15, 2019
a365316
Merge branch 'master' into 2fa-disabling-ask-password
Aug 16, 2019
5013195
Fix tests after merge
Aug 16, 2019
506fbec
Merge branch 'master' into 2fa-disabling-ask-password
brainwane Aug 19, 2019
48695a0
Merge branch 'master' into 2fa-disabling-ask-password
nlhkabu Aug 24, 2019
c54a460
Merge branch 'master' into 2fa-disabling-ask-password
Aug 24, 2019
94f483a
Remove `index` parameter from call to `confirm_modal`
Aug 24, 2019
db790e6
Reinstate id and fix missing name on modal input
Aug 25, 2019
bb1abcc
Require user password to delete account
Sep 4, 2019
d307c80
Merge branch 'master' into 2fa-disabling-ask-password
Sep 4, 2019
6bba6aa
Fix linting
Sep 4, 2019
bc5c04d
Fix sorting imports
Sep 4, 2019
6ab53a0
Merge branch 'master' into 2fa-disabling-ask-password
brainwane Sep 8, 2019
f1fa167
Reinstate index kwargs to confirm_password_button
Sep 10, 2019
618aaca
Merge branch '2fa-disabling-ask-password' of github.com:yeraydiazdiaz…
Sep 10, 2019
69443b8
Add translation to modal text
Sep 10, 2019
91dfaca
Remove index parameter from confirm_modal macros
Sep 10, 2019
dc86395
Fix call to gettext
Sep 10, 2019
b7c3e2f
Merge branch 'master' into 2fa-disabling-ask-password
Oct 5, 2019
fd394f3
Add tests for ModalClose controller
Oct 5, 2019
da854b7
Merge branch 'master' into 2fa-disabling-ask-password
di Oct 7, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions tests/unit/manage/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ def test_creation(self):

assert form.user_service is user_service

def test_validate_confirm_password(self):
user_service = pretend.stub(
find_userid=pretend.call_recorder(lambda userid: 1),
check_password=pretend.call_recorder(
lambda userid, password, tags=None: True
),
)
form = forms.DeleteTOTPForm(
username="username", user_service=user_service, password="password"
)

assert form.validate()


class TestProvisionWebAuthnForm:
def test_creation(self):
Expand Down Expand Up @@ -433,16 +446,26 @@ def test_validate_token_scope_valid_project(self):
class TestDeleteMacaroonForm:
def test_creation(self):
macaroon_service = pretend.stub()
form = forms.DeleteMacaroonForm(macaroon_service=macaroon_service)
user_service = pretend.stub()
form = forms.DeleteMacaroonForm(
macaroon_service=macaroon_service, user_service=user_service
)

assert form.macaroon_service is macaroon_service
assert form.user_service is user_service

def test_validate_macaroon_id_invalid(self):
macaroon_service = pretend.stub(
find_macaroon=pretend.call_recorder(lambda id: None)
)
user_service = pretend.stub(
find_userid=lambda *a, **kw: 1, check_password=lambda *a, **kw: True
)
form = forms.DeleteMacaroonForm(
data={"macaroon_id": pretend.stub()}, macaroon_service=macaroon_service
data={"macaroon_id": pretend.stub(), "password": "password"},
macaroon_service=macaroon_service,
user_service=user_service,
username="username",
)

assert not form.validate()
Expand All @@ -452,8 +475,14 @@ def test_validate_macaroon_id(self):
macaroon_service = pretend.stub(
find_macaroon=pretend.call_recorder(lambda id: pretend.stub())
)
user_service = pretend.stub(
find_userid=lambda *a, **kw: 1, check_password=lambda *a, **kw: True
)
form = forms.DeleteMacaroonForm(
data={"macaroon_id": pretend.stub()}, macaroon_service=macaroon_service
data={"macaroon_id": pretend.stub(), "password": "password"},
macaroon_service=macaroon_service,
username="username",
user_service=user_service,
)

assert form.validate()
57 changes: 41 additions & 16 deletions tests/unit/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,9 +666,15 @@ def test_delete_account(self, monkeypatch, db_request):
jid = JournalEntryFactory.create(submitted_by=user).id

db_request.user = user
db_request.params = {"confirm_username": user.username}
db_request.params = {"confirm_password": user.password}
db_request.find_service = lambda *a, **kw: pretend.stub()

confirm_password_obj = pretend.stub(validate=lambda: True)
confirm_password_cls = pretend.call_recorder(
lambda *a, **kw: confirm_password_obj
)
monkeypatch.setattr(views, "ConfirmPasswordForm", confirm_password_cls)

monkeypatch.setattr(
views.ManageAccountViews, "default_response", pretend.stub()
)
Expand Down Expand Up @@ -697,7 +703,7 @@ def test_delete_account(self, monkeypatch, db_request):

def test_delete_account_no_confirm(self, monkeypatch):
request = pretend.stub(
params={"confirm_username": ""},
params={"confirm_password": ""},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
find_service=lambda *a, **kw: pretend.stub(),
)
Expand All @@ -715,12 +721,18 @@ def test_delete_account_no_confirm(self, monkeypatch):

def test_delete_account_wrong_confirm(self, monkeypatch):
request = pretend.stub(
params={"confirm_username": "invalid"},
params={"confirm_password": "invalid"},
user=pretend.stub(username="username"),
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
find_service=lambda *a, **kw: pretend.stub(),
)

confirm_password_obj = pretend.stub(validate=lambda: False)
confirm_password_cls = pretend.call_recorder(
lambda *a, **kw: confirm_password_obj
)
monkeypatch.setattr(views, "ConfirmPasswordForm", confirm_password_cls)

monkeypatch.setattr(
views.ManageAccountViews, "default_response", pretend.stub()
)
Expand All @@ -730,19 +742,25 @@ def test_delete_account_wrong_confirm(self, monkeypatch):
assert view.delete_account() == view.default_response
assert request.session.flash.calls == [
pretend.call(
"Could not delete account - 'invalid' is not the same as " "'username'",
"Could not delete account - Invalid credentials. Please try again.",
queue="error",
)
]

def test_delete_account_has_active_projects(self, monkeypatch):
request = pretend.stub(
params={"confirm_username": "username"},
params={"confirm_password": "password"},
user=pretend.stub(username="username"),
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
find_service=lambda *a, **kw: pretend.stub(),
)

confirm_password_obj = pretend.stub(validate=lambda: True)
confirm_password_cls = pretend.call_recorder(
lambda *a, **kw: confirm_password_obj
)
monkeypatch.setattr(views, "ConfirmPasswordForm", confirm_password_cls)

monkeypatch.setattr(
views.ManageAccountViews, "default_response", pretend.stub()
)
Expand Down Expand Up @@ -1080,7 +1098,7 @@ def test_delete_totp(self, monkeypatch, db_request):
record_event=pretend.call_recorder(lambda *a, **kw: None),
)
request = pretend.stub(
POST={"confirm_username": pretend.stub()},
POST={"confirm_password": pretend.stub()},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
find_service=lambda *a, **kw: user_service,
user=pretend.stub(
Expand Down Expand Up @@ -1123,13 +1141,13 @@ def test_delete_totp(self, monkeypatch, db_request):
)
]

def test_delete_totp_bad_username(self, monkeypatch, db_request):
def test_delete_totp_bad_password(self, monkeypatch, db_request):
user_service = pretend.stub(
get_totp_secret=lambda id: b"secret",
update_user=pretend.call_recorder(lambda *a, **kw: None),
)
request = pretend.stub(
POST={"confirm_username": pretend.stub()},
POST={"confirm_password": pretend.stub()},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
find_service=lambda *a, **kw: user_service,
user=pretend.stub(
Expand All @@ -1151,7 +1169,7 @@ def test_delete_totp_bad_username(self, monkeypatch, db_request):

assert user_service.update_user.calls == []
assert request.session.flash.calls == [
pretend.call("Invalid credentials", queue="error")
pretend.call("Invalid credentials. Try again", queue="error")
]
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/foo/bar/"
Expand All @@ -1162,7 +1180,7 @@ def test_delete_totp_not_provisioned(self, monkeypatch, db_request):
update_user=pretend.call_recorder(lambda *a, **kw: None),
)
request = pretend.stub(
POST={"confirm_username": pretend.stub()},
POST={"confirm_password": pretend.stub()},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
find_service=lambda *a, **kw: user_service,
user=pretend.stub(
Expand Down Expand Up @@ -1469,7 +1487,7 @@ def test_default_response(self, monkeypatch):
)

request = pretend.stub(
user=pretend.stub(id=pretend.stub()),
user=pretend.stub(id=pretend.stub(), username=pretend.stub()),
find_service=lambda interface, **kw: {
IMacaroonService: pretend.stub(),
IUserService: pretend.stub(),
Expand Down Expand Up @@ -1767,14 +1785,16 @@ def test_delete_macaroon_invalid_form(self, monkeypatch):
delete_macaroon=pretend.call_recorder(lambda id: pretend.stub())
)
request = pretend.stub(
POST={},
POST={"confirm_password": "password", "macaroon_id": "macaroon_id"},
route_path=pretend.call_recorder(lambda x: pretend.stub()),
find_service=lambda interface, **kw: {
IMacaroonService: macaroon_service,
IUserService: pretend.stub(),
}[interface],
referer="/fake/safe/route",
host=None,
user=pretend.stub(username=pretend.stub()),
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
)

delete_macaroon_obj = pretend.stub(validate=lambda: False)
Expand All @@ -1790,20 +1810,25 @@ def test_delete_macaroon_invalid_form(self, monkeypatch):
assert isinstance(result, HTTPSeeOther)
assert result.location == "/fake/safe/route"
assert macaroon_service.delete_macaroon.calls == []
assert request.session.flash.calls == [
pretend.call("Invalid credentials. Try again", queue="error")
]

def test_delete_macaroon_dangerous_redirect(self, monkeypatch):
macaroon_service = pretend.stub(
delete_macaroon=pretend.call_recorder(lambda id: pretend.stub())
)
request = pretend.stub(
POST={},
POST={"confirm_password": "password", "macaroon_id": "macaroon_id"},
route_path=pretend.call_recorder(lambda x: "/safe/route"),
find_service=lambda interface, **kw: {
IMacaroonService: macaroon_service,
IUserService: pretend.stub(),
}[interface],
referer="http://google.com/",
host=None,
user=pretend.stub(username=pretend.stub()),
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
)

delete_macaroon_obj = pretend.stub(validate=lambda: False)
Expand Down Expand Up @@ -1833,7 +1858,7 @@ def test_delete_macaroon(self, monkeypatch):
)
user_service = pretend.stub(record_event=record_event)
request = pretend.stub(
POST={},
POST={"confirm_password": "password", "macaroon_id": "macaroon_id"},
route_path=pretend.call_recorder(lambda x: pretend.stub()),
find_service=lambda interface, **kw: {
IMacaroonService: macaroon_service,
Expand All @@ -1842,7 +1867,7 @@ def test_delete_macaroon(self, monkeypatch):
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
referer="/fake/safe/route",
host=None,
user=pretend.stub(id=pretend.stub()),
user=pretend.stub(id=pretend.stub(), username=pretend.stub()),
remote_addr="0.0.0.0",
)

Expand Down Expand Up @@ -1892,7 +1917,7 @@ def test_delete_macaroon_records_events_for_each_project(self, monkeypatch):
)
user_service = pretend.stub(record_event=record_event)
request = pretend.stub(
POST={},
POST={"confirm_password": pretend.stub(), "macaroon_id": pretend.stub()},
route_path=pretend.call_recorder(lambda x: pretend.stub()),
find_service=lambda interface, **kw: {
IMacaroonService: macaroon_service,
Expand Down
16 changes: 11 additions & 5 deletions warehouse/manage/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,20 @@ def __init__(self, *args, user_service, **kwargs):
self.user_service = user_service


class DeleteTOTPForm(UsernameMixin, forms.Form):
class ConfirmPasswordForm(UsernameMixin, PasswordMixin, forms.Form):

__params__ = ["confirm_username"]
__params__ = ["confirm_password"]

def __init__(self, *args, user_service, **kwargs):
super().__init__(*args, **kwargs)
self.user_service = user_service


class DeleteTOTPForm(ConfirmPasswordForm):
# TODO: delete?
pass


class ProvisionTOTPForm(TOTPValueMixin, forms.Form):

__params__ = ["totp_value"]
Expand Down Expand Up @@ -246,15 +251,16 @@ def validate_token_scope(self, field):
self.validated_scope = {"projects": [scope_value]}


class DeleteMacaroonForm(forms.Form):
__params__ = ["macaroon_id"]
class DeleteMacaroonForm(UsernameMixin, PasswordMixin, forms.Form):
__params__ = ["confirm_password", "macaroon_id"]

macaroon_id = wtforms.StringField(
validators=[wtforms.validators.DataRequired(message="Identifier required")]
)

def __init__(self, *args, macaroon_service, **kwargs):
def __init__(self, *args, macaroon_service, user_service, **kwargs):
super().__init__(*args, **kwargs)
self.user_service = user_service
self.macaroon_service = macaroon_service

def validate_macaroon_id(self, field):
Expand Down
35 changes: 24 additions & 11 deletions warehouse/manage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
AddEmailForm,
ChangePasswordForm,
ChangeRoleForm,
ConfirmPasswordForm,
CreateMacaroonForm,
CreateRoleForm,
DeleteMacaroonForm,
Expand Down Expand Up @@ -304,18 +305,22 @@ def change_password(self):

return {**self.default_response, "change_password_form": form}

@view_config(request_method="POST", request_param=["confirm_username"])
@view_config(request_method="POST", request_param=DeleteTOTPForm.__params__)
def delete_account(self):
username = self.request.params.get("confirm_username")

if not username:
confirm_password = self.request.params.get("confirm_password")
if not confirm_password:
self.request.session.flash("Confirm the request", queue="error")
return self.default_response

if username != self.request.user.username:
form = ConfirmPasswordForm(
password=confirm_password,
username=self.request.user.username,
user_service=self.user_service,
)

if not form.validate():
self.request.session.flash(
f"Could not delete account - {username!r} is not the same as "
f"{self.request.user.username!r}",
f"Could not delete account - Invalid credentials. Please try again.",
queue="error",
)
return self.default_response
Expand Down Expand Up @@ -470,7 +475,7 @@ def delete_totp(self):
return HTTPSeeOther(self.request.route_path("manage.account"))

form = DeleteTOTPForm(
**self.request.POST,
password=self.request.POST["confirm_password"],
username=self.request.user.username,
user_service=self.user_service,
)
Expand All @@ -489,7 +494,7 @@ def delete_totp(self):
queue="success",
)
else:
self.request.session.flash("Invalid credentials", queue="error")
self.request.session.flash("Invalid credentials. Try again", queue="error")

return HTTPSeeOther(self.request.route_path("manage.account"))

Expand Down Expand Up @@ -631,7 +636,9 @@ def default_response(self):
project_names=self.project_names,
),
"delete_macaroon_form": DeleteMacaroonForm(
macaroon_service=self.macaroon_service
username=self.request.user.username,
user_service=self.user_service,
macaroon_service=self.macaroon_service,
),
}

Expand Down Expand Up @@ -699,7 +706,11 @@ def create_macaroon(self):
@view_config(request_method="POST", request_param=DeleteMacaroonForm.__params__)
def delete_macaroon(self):
form = DeleteMacaroonForm(
**self.request.POST, macaroon_service=self.macaroon_service
password=self.request.POST["confirm_password"],
macaroon_id=self.request.POST["macaroon_id"],
macaroon_service=self.macaroon_service,
username=self.request.user.username,
user_service=self.user_service,
)

if form.validate():
Expand Down Expand Up @@ -730,6 +741,8 @@ def delete_macaroon(self):
self.request.session.flash(
f"Deleted API token '{macaroon.description}'.", queue="success"
)
else:
self.request.session.flash("Invalid credentials. Try again", queue="error")

redirect_to = self.request.referer
if not is_safe_url(redirect_to, host=self.request.host):
Expand Down
Loading