forked from Github-Mirrors/canaille
Password mechanism recovery. Fixes #3
This commit is contained in:
parent
7747a64570
commit
2fc6af0fc9
10 changed files with 327 additions and 18 deletions
|
@ -1,8 +1,12 @@
|
||||||
|
import email.message
|
||||||
|
import hashlib
|
||||||
|
import smtplib
|
||||||
|
|
||||||
from flask import Blueprint, request, flash, url_for, current_app
|
from flask import Blueprint, request, flash, url_for, current_app
|
||||||
from flask import render_template, redirect
|
from flask import render_template, redirect
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from .forms import LoginForm, ProfileForm
|
from .forms import LoginForm, ProfileForm, PasswordResetForm, ForgottenPasswordForm
|
||||||
from .flaskutils import current_user, user_needed
|
from .flaskutils import current_user, user_needed
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
@ -25,7 +29,7 @@ def login():
|
||||||
if not form.validate() or not User.authenticate(
|
if not form.validate() or not User.authenticate(
|
||||||
form.login.data, form.password.data, True
|
form.login.data, form.password.data, True
|
||||||
):
|
):
|
||||||
flash(gettext("Login failed, please check your information"), "error")
|
flash(_("Login failed, please check your information"), "error")
|
||||||
return render_template("login.html", form=form)
|
return render_template("login.html", form=form)
|
||||||
|
|
||||||
return redirect(url_for("canaille.account.index"))
|
return redirect(url_for("canaille.account.index"))
|
||||||
|
@ -54,7 +58,7 @@ def profile(user):
|
||||||
|
|
||||||
if request.form:
|
if request.form:
|
||||||
if not form.validate():
|
if not form.validate():
|
||||||
flash(gettext("Profile edition failed."), "error")
|
flash(_("Profile edition failed."), "error")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for attribute in form:
|
for attribute in form:
|
||||||
|
@ -65,8 +69,106 @@ def profile(user):
|
||||||
user[model_attribute_name] = [attribute.data]
|
user[model_attribute_name] = [attribute.data]
|
||||||
|
|
||||||
if not form.password1.data or user.set_password(form.password1.data):
|
if not form.password1.data or user.set_password(form.password1.data):
|
||||||
flash(gettext("Profile updated successfuly."), "success")
|
flash(_("Profile updated successfuly."), "success")
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
return render_template("profile.html", form=form, menuitem="profile")
|
return render_template("profile.html", form=form, menuitem="profile")
|
||||||
|
|
||||||
|
|
||||||
|
def profile_hash(user, password):
|
||||||
|
return hashlib.sha256(
|
||||||
|
current_app.config["SECRET_KEY"].encode("utf-8")
|
||||||
|
+ user.encode("utf-8")
|
||||||
|
+ password.encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/reset", methods=["GET", "POST"])
|
||||||
|
def forgotten():
|
||||||
|
form = ForgottenPasswordForm(request.form)
|
||||||
|
if not request.form:
|
||||||
|
return render_template("forgotten-password.html", form=form)
|
||||||
|
|
||||||
|
if not form.validate():
|
||||||
|
flash(_("Could not send the password reset link."), "error")
|
||||||
|
return render_template("forgotten-password.html", form=form)
|
||||||
|
|
||||||
|
user = User.get(form.login.data)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
flash(
|
||||||
|
_("A password reset link has been sent at your email address."), "success"
|
||||||
|
)
|
||||||
|
return render_template("forgotten-password.html", form=form)
|
||||||
|
|
||||||
|
recipient = user.mail
|
||||||
|
base_url = current_app.config.get("URL") or request.base_url
|
||||||
|
url = base_url + url_for(
|
||||||
|
"canaille.account.reset",
|
||||||
|
uid=user.uid,
|
||||||
|
hash=profile_hash(user.uid[0], user.userPassword[0]),
|
||||||
|
)
|
||||||
|
subject = _("Password reset on {website_name}").format(
|
||||||
|
website_name=current_app.config.get("NAME", url)
|
||||||
|
)
|
||||||
|
text_body = _(
|
||||||
|
"To reset your password on {website_name}, visit the following link :\n{url}"
|
||||||
|
).format(website_name=current_app.config.get("NAME", url), url=url)
|
||||||
|
|
||||||
|
msg = email.message.EmailMessage()
|
||||||
|
msg.set_content(text_body)
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = current_app.config["SMTP"]["FROM_ADDR"]
|
||||||
|
msg["To"] = recipient
|
||||||
|
|
||||||
|
success = True
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP(
|
||||||
|
host=current_app.config["SMTP"]["HOST"],
|
||||||
|
port=current_app.config["SMTP"]["PORT"],
|
||||||
|
) as smtp:
|
||||||
|
if current_app.config["SMTP"].get("TLS"):
|
||||||
|
smtp.starttls()
|
||||||
|
if current_app.config["SMTP"].get("LOGIN"):
|
||||||
|
smtp.login(
|
||||||
|
user=current_app.config["SMTP"]["LOGIN"],
|
||||||
|
password=current_app.config["SMTP"].get("PASSWORD"),
|
||||||
|
)
|
||||||
|
smtp.send_message(msg)
|
||||||
|
|
||||||
|
except smtplib.SMTPRecipientsRefused:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
flash(_("Could not reset your password"), "error")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if success:
|
||||||
|
flash(
|
||||||
|
_("A password reset link has been sent at your email address."), "success"
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("forgotten-password.html", form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/reset/<uid>/<hash>", methods=["GET", "POST"])
|
||||||
|
def reset(uid, hash):
|
||||||
|
form = PasswordResetForm(request.form)
|
||||||
|
user = User.get(uid)
|
||||||
|
|
||||||
|
if not user or hash != profile_hash(user.uid[0], user.userPassword[0]):
|
||||||
|
flash(
|
||||||
|
_("The password reset link that brought you here was invalid."),
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return redirect(url_for("canaille.account.index"))
|
||||||
|
|
||||||
|
if request.form and form.validate():
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
user.login()
|
||||||
|
|
||||||
|
flash(_("Your password has been updated successfuly"), "success")
|
||||||
|
return redirect(url_for("canaille.account.profile", user_id=uid))
|
||||||
|
|
||||||
|
return render_template("reset-password.html", form=form, uid=uid, hash=hash)
|
||||||
|
|
|
@ -4,6 +4,9 @@ SECRET_KEY = "change me before you go in production"
|
||||||
# Your organization name.
|
# Your organization name.
|
||||||
NAME = "MyDomain"
|
NAME = "MyDomain"
|
||||||
|
|
||||||
|
# The interface on which canaille will be served
|
||||||
|
# URL = "https://auth.mydomain.tld"
|
||||||
|
|
||||||
# You can display a logo to be recognized on login screens
|
# You can display a logo to be recognized on login screens
|
||||||
# LOGO = "https://path/to/your/organization/logo.png"
|
# LOGO = "https://path/to/your/organization/logo.png"
|
||||||
|
|
||||||
|
@ -59,3 +62,11 @@ FAMILY_NAME = "sn"
|
||||||
PREFERRED_USERNAME = "displayName"
|
PREFERRED_USERNAME = "displayName"
|
||||||
LOCALE = "preferredLanguage"
|
LOCALE = "preferredLanguage"
|
||||||
PICTURE = "photo"
|
PICTURE = "photo"
|
||||||
|
|
||||||
|
[SMTP]
|
||||||
|
HOST = "localhost"
|
||||||
|
PORT = 25
|
||||||
|
TLS = false
|
||||||
|
LOGIN = "smtp_user"
|
||||||
|
PASSWORD = "smtp_password"
|
||||||
|
FROM_ADDR = "admin@mydomain.tld"
|
||||||
|
|
|
@ -8,7 +8,7 @@ class LoginForm(FlaskForm):
|
||||||
_("Login"),
|
_("Login"),
|
||||||
validators=[wtforms.validators.DataRequired()],
|
validators=[wtforms.validators.DataRequired()],
|
||||||
render_kw={
|
render_kw={
|
||||||
"placeholder": "mdupont",
|
"placeholder": _("jane@doe.com"),
|
||||||
"spellcheck": "false",
|
"spellcheck": "false",
|
||||||
"autocorrect": "off",
|
"autocorrect": "off",
|
||||||
"inputmode": "email",
|
"inputmode": "email",
|
||||||
|
@ -20,6 +20,32 @@ class LoginForm(FlaskForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ForgottenPasswordForm(FlaskForm):
|
||||||
|
login = wtforms.StringField(
|
||||||
|
_("Login"),
|
||||||
|
validators=[wtforms.validators.DataRequired()],
|
||||||
|
render_kw={
|
||||||
|
"placeholder": _("jane@doe.com"),
|
||||||
|
"spellcheck": "false",
|
||||||
|
"autocorrect": "off",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetForm(FlaskForm):
|
||||||
|
password = wtforms.PasswordField(
|
||||||
|
_("Password"), validators=[wtforms.validators.DataRequired()]
|
||||||
|
)
|
||||||
|
confirmation = wtforms.PasswordField(
|
||||||
|
_("Password confirmation"),
|
||||||
|
validators=[
|
||||||
|
wtforms.validators.EqualTo(
|
||||||
|
"password", _("Password and confirmation do not match.")
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(FlaskForm):
|
class ProfileForm(FlaskForm):
|
||||||
sub = wtforms.StringField(
|
sub = wtforms.StringField(
|
||||||
_("Username"),
|
_("Username"),
|
||||||
|
@ -73,4 +99,4 @@ class ProfileForm(FlaskForm):
|
||||||
|
|
||||||
def validate_password2(self, field):
|
def validate_password2(self, field):
|
||||||
if self.password1.data and self.password1.data != field.data:
|
if self.password1.data and self.password1.data != field.data:
|
||||||
raise wtforms.ValidationError(_("Password and confirmation are different."))
|
raise wtforms.ValidationError(_("Password and confirmation do not match."))
|
||||||
|
|
40
canaille/templates/forgotten-password.html
Normal file
40
canaille/templates/forgotten-password.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="loginform">
|
||||||
|
<h3 class="ui top attached header">
|
||||||
|
{% trans %}Forgotten password{% endtrans %}
|
||||||
|
</h3>
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="ui attached message {{ category }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="ui attached message">
|
||||||
|
{% trans %}
|
||||||
|
After this form is sent, if the email address or the login you provided
|
||||||
|
exists, you will receive an email containing a link that will allow you
|
||||||
|
to reset your password.
|
||||||
|
{% endtrans %}
|
||||||
|
</div>
|
||||||
|
<div class="ui attached clearing segment">
|
||||||
|
<form method="POST"
|
||||||
|
id="{{ form.id or form.__class__.__name__|lower }}"
|
||||||
|
action="{{ request.url }}"
|
||||||
|
role="form"
|
||||||
|
class="ui form"
|
||||||
|
>
|
||||||
|
|
||||||
|
{{ form.hidden_tag() if form.hidden_tag }}
|
||||||
|
{{ sui.render_field(form.login, icon="user") }}
|
||||||
|
|
||||||
|
<button type="submit" class="ui right floated primary button">{{ _("Sign in") }}</button>
|
||||||
|
<a type="button" class="ui right floated button" href="{{ url_for('canaille.account.login') }}">{{ _("Login page") }}</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
{{ sui.render_field(form.password, icon="lock") }}
|
{{ sui.render_field(form.password, icon="lock") }}
|
||||||
|
|
||||||
<button type="submit" class="ui right floated primary button">{{ _("Sign in") }}</button>
|
<button type="submit" class="ui right floated primary button">{{ _("Sign in") }}</button>
|
||||||
|
<a type="button" class="ui right floated button" href="{{ url_for('canaille.account.forgotten') }}">{{ _("Forgotten password") }}</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
20
canaille/templates/reset-password.html
Normal file
20
canaille/templates/reset-password.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="loginform">
|
||||||
|
<h3 class="ui top attached header">
|
||||||
|
{% trans %}Password reset{% endtrans %}
|
||||||
|
</h3>
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="ui attached message {{ category }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
<div class="ui attached clearing segment">
|
||||||
|
{{ sui.render_form(form, _("Password reset"), action=url_for("canaille.account.reset", uid=uid, hash=hash)) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -26,7 +26,7 @@ sn: Doe
|
||||||
uid: admin
|
uid: admin
|
||||||
mail: admin@mydomain.tld
|
mail: admin@mydomain.tld
|
||||||
telephoneNumber: 555-000-000
|
telephoneNumber: 555-000-000
|
||||||
userpassword: {SSHA}7zQVLckaEc6cJEsS0ylVipvb2PAR/4tS
|
userPassword: {SSHA}7zQVLckaEc6cJEsS0ylVipvb2PAR/4tS
|
||||||
memberof: cn=admins,ou=groups,dc=mydomain,dc=tld
|
memberof: cn=admins,ou=groups,dc=mydomain,dc=tld
|
||||||
memberof: cn=users,ou=groups,dc=mydomain,dc=tld
|
memberof: cn=users,ou=groups,dc=mydomain,dc=tld
|
||||||
|
|
||||||
|
@ -39,5 +39,5 @@ sn: Doe
|
||||||
uid: user
|
uid: user
|
||||||
mail: user@mydomain.tld
|
mail: user@mydomain.tld
|
||||||
telephoneNumber: 555-000-001
|
telephoneNumber: 555-000-001
|
||||||
userpassword: {SSHA}Yr1ZxSljRsKyaTB30suY2iZ1KRTStF1X
|
userPassword: {SSHA}Yr1ZxSljRsKyaTB30suY2iZ1KRTStF1X
|
||||||
memberof: cn=users,ou=groups,dc=mydomain,dc=tld
|
memberof: cn=users,ou=groups,dc=mydomain,dc=tld
|
||||||
|
|
|
@ -53,14 +53,16 @@ commands = {envbindir}/pytest --showlocals --full-trace {posargs}
|
||||||
deps =
|
deps =
|
||||||
--editable .
|
--editable .
|
||||||
flask-webtest
|
flask-webtest
|
||||||
pytest
|
mock
|
||||||
pdbpp
|
pdbpp
|
||||||
|
pytest
|
||||||
|
|
||||||
[testenv:coverage]
|
[testenv:coverage]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
--editable .
|
--editable .
|
||||||
flask-webtest
|
flask-webtest
|
||||||
|
mock
|
||||||
pdbpp
|
pdbpp
|
||||||
pytest
|
pytest
|
||||||
pytest-coverage
|
pytest-coverage
|
||||||
|
|
|
@ -149,6 +149,14 @@ def app(slapd_server, keypair_path):
|
||||||
"PICTURE": "photo",
|
"PICTURE": "photo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"SMTP": {
|
||||||
|
"HOST": "localhost",
|
||||||
|
"PORT": 25,
|
||||||
|
"TLS": False,
|
||||||
|
"LOGIN": "smtp_login",
|
||||||
|
"PASSWORD": "smtp_password",
|
||||||
|
"FROM_ADDR": "admin@mydomain.tld",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return app
|
return app
|
||||||
|
@ -224,7 +232,7 @@ def user(app, slapd_connection):
|
||||||
sn="Doe",
|
sn="Doe",
|
||||||
uid="user",
|
uid="user",
|
||||||
mail="john@doe.com",
|
mail="john@doe.com",
|
||||||
userpassword="{SSHA}fw9DYeF/gHTHuVMepsQzVYAkffGcU8Fz",
|
userPassword="{SSHA}fw9DYeF/gHTHuVMepsQzVYAkffGcU8Fz",
|
||||||
)
|
)
|
||||||
u.save(slapd_connection)
|
u.save(slapd_connection)
|
||||||
return u
|
return u
|
||||||
|
@ -239,7 +247,7 @@ def admin(app, slapd_connection):
|
||||||
sn="Doe",
|
sn="Doe",
|
||||||
uid="admin",
|
uid="admin",
|
||||||
mail="jane@doe.com",
|
mail="jane@doe.com",
|
||||||
userpassword="{SSHA}Vmgh2jkD0idX3eZHf8RzGos31oerjGiU",
|
userPassword="{SSHA}Vmgh2jkD0idX3eZHf8RzGos31oerjGiU",
|
||||||
)
|
)
|
||||||
u.save(slapd_connection)
|
u.save(slapd_connection)
|
||||||
return u
|
return u
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
def test_login_and_out(testclient, slapd_connection, user, client):
|
import mock
|
||||||
|
from canaille.account import profile_hash
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_and_out(testclient, slapd_connection, user):
|
||||||
with testclient.session_transaction() as session:
|
with testclient.session_transaction() as session:
|
||||||
assert session.get("user_dn") is None
|
assert session.get("user_dn") is None
|
||||||
|
|
||||||
|
@ -24,7 +28,7 @@ def test_login_and_out(testclient, slapd_connection, user, client):
|
||||||
assert session.get("user_dn") is None
|
assert session.get("user_dn") is None
|
||||||
|
|
||||||
|
|
||||||
def test_login_wrong_password(testclient, slapd_connection, user, client):
|
def test_login_wrong_password(testclient, slapd_connection, user):
|
||||||
with testclient.session_transaction() as session:
|
with testclient.session_transaction() as session:
|
||||||
assert session.get("user_dn") is None
|
assert session.get("user_dn") is None
|
||||||
|
|
||||||
|
@ -38,7 +42,7 @@ def test_login_wrong_password(testclient, slapd_connection, user, client):
|
||||||
assert b"Login failed, please check your information" in res.body
|
assert b"Login failed, please check your information" in res.body
|
||||||
|
|
||||||
|
|
||||||
def test_login_no_password(testclient, slapd_connection, user, client):
|
def test_login_no_password(testclient, slapd_connection, user):
|
||||||
with testclient.session_transaction() as session:
|
with testclient.session_transaction() as session:
|
||||||
assert session.get("user_dn") is None
|
assert session.get("user_dn") is None
|
||||||
|
|
||||||
|
@ -52,7 +56,7 @@ def test_login_no_password(testclient, slapd_connection, user, client):
|
||||||
assert b"Login failed, please check your information" in res.body
|
assert b"Login failed, please check your information" in res.body
|
||||||
|
|
||||||
|
|
||||||
def test_login_with_alternate_attribute(testclient, slapd_connection, user, client):
|
def test_login_with_alternate_attribute(testclient, slapd_connection, user):
|
||||||
res = testclient.get("/login")
|
res = testclient.get("/login")
|
||||||
assert 200 == res.status_code
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
@ -66,3 +70,98 @@ def test_login_with_alternate_attribute(testclient, slapd_connection, user, clie
|
||||||
|
|
||||||
with testclient.session_transaction() as session:
|
with testclient.session_transaction() as session:
|
||||||
assert user.dn == session.get("user_dn")
|
assert user.dn == session.get("user_dn")
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("smtplib.SMTP")
|
||||||
|
def test_password_forgotten(SMTP, testclient, slapd_connection, user):
|
||||||
|
res = testclient.get("/reset")
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
res.form["login"] = "user"
|
||||||
|
res = res.form.submit()
|
||||||
|
assert 200 == res.status_code
|
||||||
|
assert "A password reset link has been sent at your email address." in res.text
|
||||||
|
|
||||||
|
SMTP.assert_called_once_with(host="localhost", port=25)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("smtplib.SMTP")
|
||||||
|
def test_password_forgotten_invalid_form(SMTP, testclient, slapd_connection, user):
|
||||||
|
res = testclient.get("/reset")
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
res.form["login"] = ""
|
||||||
|
res = res.form.submit()
|
||||||
|
assert 200 == res.status_code
|
||||||
|
assert "Could not send the password reset link." in res.text
|
||||||
|
|
||||||
|
SMTP.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("smtplib.SMTP")
|
||||||
|
def test_password_forgotten_invalid(SMTP, testclient, slapd_connection, user):
|
||||||
|
res = testclient.get("/reset")
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
res.form["login"] = "i-dont-really-exist"
|
||||||
|
res = res.form.submit()
|
||||||
|
assert 200 == res.status_code
|
||||||
|
assert "A password reset link has been sent at your email address." in res.text
|
||||||
|
|
||||||
|
SMTP.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_reset(testclient, slapd_connection, user):
|
||||||
|
user.attr_type_by_name(conn=slapd_connection)
|
||||||
|
user.reload(conn=slapd_connection)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
hash = profile_hash("user", user.userPassword[0])
|
||||||
|
|
||||||
|
res = testclient.get("/reset/user/" + hash)
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
res.form["password"] = "foobarbaz"
|
||||||
|
res.form["confirmation"] = "foobarbaz"
|
||||||
|
res = res.form.submit()
|
||||||
|
assert 302 == res.status_code
|
||||||
|
|
||||||
|
res = res.follow()
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
with testclient.app.app_context():
|
||||||
|
assert user.check_password("foobarbaz")
|
||||||
|
assert "Your password has been updated successfuly" in res.text
|
||||||
|
user.set_password("correct horse battery staple", conn=slapd_connection)
|
||||||
|
|
||||||
|
res = testclient.get("/reset/user/" + hash)
|
||||||
|
res = res.follow()
|
||||||
|
res = res.follow()
|
||||||
|
assert "The password reset link that brought you here was invalid." in res.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_reset_bad_link(testclient, slapd_connection, user):
|
||||||
|
user.attr_type_by_name(conn=slapd_connection)
|
||||||
|
user.reload(conn=slapd_connection)
|
||||||
|
|
||||||
|
res = testclient.get("/reset/user/foobarbaz")
|
||||||
|
res = res.follow()
|
||||||
|
res = res.follow()
|
||||||
|
assert "The password reset link that brought you here was invalid." in res.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_reset_bad_password(testclient, slapd_connection, user):
|
||||||
|
user.attr_type_by_name(conn=slapd_connection)
|
||||||
|
user.reload(conn=slapd_connection)
|
||||||
|
with testclient.app.app_context():
|
||||||
|
hash = profile_hash("user", user.userPassword[0])
|
||||||
|
|
||||||
|
res = testclient.get("/reset/user/" + hash)
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
res.form["password"] = "foobarbaz"
|
||||||
|
res.form["confirmation"] = "typo"
|
||||||
|
res = res.form.submit()
|
||||||
|
assert 200 == res.status_code
|
||||||
|
|
||||||
|
with testclient.app.app_context():
|
||||||
|
assert user.check_password("correct horse battery staple")
|
||||||
|
|
Loading…
Reference in a new issue