Skip to content

Commit

Permalink
Merge branch 'master' into auto-migrate
Browse files Browse the repository at this point in the history
  • Loading branch information
dstufft authored Apr 20, 2017
2 parents b5515f9 + c1c219d commit 80e81d4
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 37 deletions.
1 change: 1 addition & 0 deletions requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ transaction
whitenoise
WTForms>=2.0.0
zope.sqlalchemy
zxcvbn-python
8 changes: 5 additions & 3 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ bleach==2.0.0 \
boto3==1.4.4 \
--hash=sha256:5050c29353fec97301116386f469fa5858ccf47201623b53cf9f74e603bda52f \
--hash=sha256:518f724c4758e5a5bed114fbcbd1cf470a15306d416ff421a025b76f1d390939
botocore==1.5.40 \
--hash=sha256:ad8ba09dfe5233c4a0099edec7cfadd240d6c232bd71684ebb28c752198c20a3 \
--hash=sha256:512a0a612e93142ed6b698740e1a40df7d899ddd79c297e13336d1048ac6d95a
botocore==1.5.41 \
--hash=sha256:0a465fd2de0345d8e921350cace9dd231f2773c4cb3827e1d6f83d5240431ce4 \
--hash=sha256:ba3ebbc86077316d25dff47a745123ab4b89f59fb31b9d64cba8c5c069c5e347
cachetools==2.0.0 \
--hash=sha256:cc4cb596b399c292a37ffdfdc713463da4feb81e18ca663e1ba2f89436cae0c1 \
--hash=sha256:715a7202240dc20dbe83abdb2d804d543e2d4f07af146f53c82166bd75f3a628
Expand Down Expand Up @@ -317,3 +317,5 @@ zope.interface==4.3.3 \
--hash=sha256:8780ef68ca8c3fe1abb30c058a59015129d6e04a6b02c2e56b9c7de6078dfa88
zope.sqlalchemy==0.7.7 \
--hash=sha256:5da8ff6b060f3a47fc0cbc61cfd6a83b959b5e730f95e492edcf7b9bf3ec987a
zxcvbn-python==4.4.14 \
--hash=sha256:fd3a46536035851571e3f4142b64d6e7bcf0ade3cd40d8fecae7a1243945e327
2 changes: 1 addition & 1 deletion tests/unit/accounts/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def test_username_exists(self):
def test_password_strength(self):
cases = (
("foobar", False),
("somethingalittlebetter9", False),
("somethingalittlebetter9", True),
("1aDeCent!1", True),
)
for pwd, valid in cases:
Expand Down
40 changes: 39 additions & 1 deletion tests/unit/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

from wtforms.validators import StopValidation, ValidationError

from warehouse.forms import Form, DBForm, URIValidator
from warehouse.forms import (
Form, DBForm, URIValidator, PasswordStrengthValidator,
)


class TestURIValidator:
Expand Down Expand Up @@ -49,6 +51,42 @@ def test_plain_schemes(self):
validator(pretend.stub(), pretend.stub(data="ftp://example.com/"))


class TestPasswordStrengthValidator:

def test_invalid_fields(self):
validator = PasswordStrengthValidator(user_input_fields=["foo"])
with pytest.raises(ValidationError) as exc:
validator({}, pretend.stub())
assert str(exc.value) == "Invalid field name: 'foo'"

@pytest.mark.parametrize("password", ["this is a great password!"])
def test_good_passwords(self, password):
validator = PasswordStrengthValidator()
validator(pretend.stub(), pretend.stub(data=password))

@pytest.mark.parametrize(
("password", "expected"),
[
(
"qwerty",
("This is a top-10 common password. Add another word or two. "
"Uncommon words are better."),
),
(
"bombo!b",
("Password is too easily guessed. Add another word or two. "
"Uncommon words are better."),
),
("bombo!b asdadad", "Password is too easily guessed."),
],
)
def test_invalid_password(self, password, expected):
validator = PasswordStrengthValidator(required_strength=5)
with pytest.raises(ValidationError) as exc:
validator(pretend.stub(), pretend.stub(data=password))
assert str(exc.value) == expected


def _raiser(exc):
raise exc

Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ def add_policy(name, filename):
"/_includes/current-user-indicator/",
domain=warehouse,
),
pretend.call(
"includes.flash-messages",
"/_includes/flash-messages/",
domain=warehouse,
),
pretend.call("search", "/search/", domain=warehouse),
pretend.call(
"accounts.profile",
Expand Down
32 changes: 8 additions & 24 deletions warehouse/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
# 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 re

import disposable_email_domains
import wtforms
Expand Down Expand Up @@ -38,22 +37,15 @@ def __init__(self, *args, user_service, **kwargs):
self.user_service = user_service


# XXX: This is a naive password strength validator, but something that can
# easily be replicated in JS for client-side feedback.
# see: https://github.com/pypa/warehouse/issues/6
PWD_MIN_LEN = 8
PWD_RE = re.compile(r"""
^ # start
(?=.*[A-Z]+.*) # >= 1 upper case
(?=.*[a-z]+.*) # >= 1 lower case
(?=.*[0-9]+.*) # >= 1 number
(?=.*[.*~`\!@#$%^&\*\(\)_+-={}|\[\]\\:";'<>?,\./]+.*) # >= 1 special char
.{""" + str(PWD_MIN_LEN) + """,} # >= 8 chars
$ # end
""", re.X)


class RegistrationForm(CredentialsMixin, forms.Form):
password = wtforms.PasswordField(
validators=[
wtforms.validators.DataRequired(),
forms.PasswordStrengthValidator(
user_input_fields=["full_name", "username", "email"],
),
],
)
password_confirm = wtforms.PasswordField(
validators=[
wtforms.validators.DataRequired(),
Expand Down Expand Up @@ -101,14 +93,6 @@ def validate_g_recaptcha_response(self, field):
# don't want to provide the user with any detail
raise wtforms.validators.ValidationError("Recaptcha error.")

def validate_password(self, field):
if not PWD_RE.match(field.data):
raise wtforms.validators.ValidationError(
"Password must contain an upper case letter, a lower case "
"letter, a number, a special character and be at least "
"%d characters in length" % PWD_MIN_LEN
)


class LoginForm(CredentialsMixin, forms.Form):
def validate_username(self, field):
Expand Down
37 changes: 37 additions & 0 deletions warehouse/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from wtforms import Form as BaseForm
from wtforms.validators import StopValidation, ValidationError
from zxcvbn import zxcvbn

from warehouse.utils.http import is_valid_uri

Expand All @@ -30,6 +31,42 @@ def __call__(self, form, field):
raise ValidationError("Invalid URI")


class PasswordStrengthValidator:

# From the zxcvbn documentation, a score of 2 is:
# somewhat guessable: protection from unthrottled online attacks.
# (guesses < 10^8)
# So we're going to require at least a score of 2 to be a valid password.
# That should (ideally) provide protection against all attacks that don't
# involve a lost database dump.
def __init__(self, *, user_input_fields=None, required_strength=2):
self.user_input_fields = user_input_fields or []
self.required_strength = required_strength

def __call__(self, form, field):
# Get all of our additional data to be used as user input to zxcvbn.
user_inputs = []
for fieldname in self.user_input_fields:
try:
user_inputs.append(form[fieldname].data)
except KeyError:
raise ValidationError(
"Invalid field name: {!r}".format(fieldname))

# Actually ask zxcvbn to check the strength of the given field's data.
results = zxcvbn(field.data, user_inputs=user_inputs)

# Determine if the score is too low, and if it is produce a nice error
# message, *hopefully* with suggestions to make the password stronger.
if results["score"] < self.required_strength:
msg = (results["feedback"]["warning"]
if results["feedback"]["warning"]
else "Password is too easily guessed.")
if results["feedback"]["suggestions"]:
msg += " " + " ".join(results["feedback"]["suggestions"])
raise ValidationError(msg)


class Form(BaseForm):

def __init__(self, *args, **kwargs):
Expand Down
5 changes: 5 additions & 0 deletions warehouse/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def includeme(config):
"/_includes/current-user-indicator/",
domain=warehouse,
)
config.add_route(
"includes.flash-messages",
"/_includes/flash-messages/",
domain=warehouse,
)

# Search Routes
config.add_route("search", "/search/", domain=warehouse)
Expand Down
16 changes: 8 additions & 8 deletions warehouse/templates/accounts/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,28 @@ <h2>Register</h2>
</ul>
{% endif %}

{% include "warehouse:templates/includes/input-credentials.html" %}

<div class="form-group">
{% if form.password_confirm.errors %}
{% if form.full_name.errors %}
<ul class="errors">
{% for error in form.password_confirm.errors %}
{% for error in form.full_name.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{{ form.password_confirm(placeholder="Confirm password", required="required") }}
{{ form.full_name(placeholder="Full name") }}
</div>

{% include "warehouse:templates/includes/input-credentials.html" %}

<div class="form-group">
{% if form.full_name.errors %}
{% if form.password_confirm.errors %}
<ul class="errors">
{% for error in form.full_name.errors %}
{% for error in form.password_confirm.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{{ form.full_name(placeholder="Full name") }}
{{ form.password_confirm(placeholder="Confirm password", required="required") }}
</div>

<div class="form-group">
Expand Down
4 changes: 4 additions & 0 deletions warehouse/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@

{% include "warehouse:templates/includes/warning-banner.html" %}

{% block flash_messages %}
{{ html_include(request.route_path("includes.flash-messages")) }}
{% endblock %}

<header class="site-header">
<div class="site-container">
<div class="split-layout">
Expand Down
31 changes: 31 additions & 0 deletions warehouse/templates/includes/flash-messages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{#
# 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.
-#}

{% for message in request.session.pop_flash(queue="error") %}
<div class="notification-bar notification-bar--warning">
<span class="notification-bar__message">{{ message }}</span>
</div>
{% endfor %}

{% for message in request.session.pop_flash() %}
<div class="notification-bar notification-bar">
<span class="notification-bar__message">{{ message }}</span>
</div>
{% endfor %}

{% for message in request.session.pop_flash(queue="success") %}
<div class="notification-bar notification-bar--success">
<span class="notification-bar__message">{{ message }}</span>
</div>
{% endfor %}
9 changes: 9 additions & 0 deletions warehouse/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,15 @@ def current_user_indicator(request):
return {}


@view_config(
route_name="includes.flash-messages",
renderer="includes/flash-messages.html",
uses_session=True,
)
def flash_messages(request):
return {}


@view_config(route_name="health", renderer="string")
def health(request):
# This will ensure that we can access the database and run queries against
Expand Down

0 comments on commit 80e81d4

Please sign in to comment.