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

Only allow latin-1 encodable characters in username and password #1402

Merged
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,4 @@ Please add a _short_ line describing the PR you make, if the PR implements a spe
## Sprint (2023-03-03 - 2023-03-17)

- PR template restructured ([#1403](https://github.com/ScilifelabDataCentre/dds_web/pull/1403))
- Only allow latin1-encodable usernames and passwords ([#1402](https://github.com/ScilifelabDataCentre/dds_web/pull/1402))
12 changes: 12 additions & 0 deletions dds_web/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ def contains_digit_or_specialchar(indata):
)


def contains_only_latin1(indata):
"""Verify that the password contains characters that can be encoded to latin-1.

Non latin-1 chars cannot be passed to requests.
"""
try:
indata.encode("latin1")
except UnicodeEncodeError as err:
raise marshmallow.ValidationError("Contains invalid characters.")


def contains_disallowed_characters(indata):
"""Indatas like <f0><9f><98><80> cause issues in Project names etc."""
disallowed = re.findall(r"[^(\w\s)]+", indata)
Expand Down Expand Up @@ -220,6 +231,7 @@ def _password_contains_valid_characters(form, field):
contains_uppercase,
contains_lowercase,
contains_digit_or_specialchar,
contains_only_latin1,
]
for val in validators:
try:
Expand Down
12 changes: 6 additions & 6 deletions tests/test_flow_invite_to_access.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest

import flask

import http
import tests
from dds_web.database import models
import dds_web.api.user
Expand Down Expand Up @@ -54,7 +54,7 @@ def perform_invite(client, inviting_user, email, role=None, project=None):
call_args = mock_token_method.call_args
invite_token = encrypted_jwt_token(*call_args.args, **call_args.kwargs)

if response.status != "200 OK":
if response.status_code != http.HTTPStatus.OK:
if DEBUG:
print(response.status_code)
raise ValueError(f"Invitation failed: {response.data}")
Expand All @@ -71,7 +71,7 @@ def get_private(client, project, auth_token):
headers=auth_token,
)
assert (
response.status == "200 OK"
response.status_code == http.HTTPStatus.OK
), f"Unable to fetch project private key for project: {project}, response: {response.data}"


Expand Down Expand Up @@ -106,7 +106,7 @@ def invite_confirm_register_and_get_private(
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

# Complete registration
form_token = flask.g.csrf_token
Expand All @@ -124,7 +124,7 @@ def invite_confirm_register_and_get_private(
headers=tests.DEFAULT_HEADER,
)

assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

user = models.User.query.filter_by(username=form_data["username"]).one_or_none()

Expand All @@ -137,7 +137,7 @@ def invite_confirm_register_and_get_private(
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

user = models.User.query.filter_by(username=form_data["username"]).one_or_none()

Expand Down
62 changes: 62 additions & 0 deletions tests/test_user_change_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,68 @@ def test_get_user_change_password_without_login(client):
assert flask.request.path == tests.DDSEndpoint.LOGIN


def test_unsuccessful_user_change_password_with_login_nonlatin1(client):
"""Non latin 1 chars should not be accepted."""
# Get user
user_auth = tests.UserAuth(tests.USER_CREDENTIALS["researcher"])

# Verify current password
user = models.User.query.get(user_auth.username)
assert user.verify_password("password")

# Get and verify variables
public_key_initial = user.public_key
nonce_initial = user.nonce
private_key_initial = user.private_key
kd_salt_initial = user.kd_salt

assert public_key_initial
assert nonce_initial
assert private_key_initial
assert kd_salt_initial

# Login
form_token = successful_web_login(client, user_auth)

# Define change password data
form_data = {
"csrf_token": form_token,
"current_password": "password",
"new_password": "123$%^qweRTY€",
"confirm_new_password": "123$%^qweRTY€",
"submit": "Change Password",
}

# Attempt to change password -- should fail
response = client.post(
tests.DDSEndpoint.CHANGE_PASSWORD,
json=form_data,
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status_code == http.HTTPStatus.OK
assert flask.request.path == tests.DDSEndpoint.CHANGE_PASSWORD

# Password should not have changed, neither should other info
assert user.verify_password("password")
assert not user.verify_password("123$%^qweRTY€")

public_key_after_password_change = user.public_key
nonce_after_password_change = user.nonce
private_key_after_password_change = user.private_key
kd_salt_after_password_change = user.kd_salt

assert public_key_after_password_change
assert nonce_after_password_change
assert private_key_after_password_change
assert kd_salt_after_password_change

assert public_key_after_password_change == public_key_initial
assert nonce_after_password_change == nonce_initial
assert private_key_after_password_change == private_key_initial
assert kd_salt_after_password_change == kd_salt_initial


def test_successful_user_change_password_with_login(client):
user_auth = tests.UserAuth(tests.USER_CREDENTIALS["researcher"])

Expand Down
93 changes: 77 additions & 16 deletions tests/test_user_confirm_invites_and_register.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import tests
import http
import flask
import pytest
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
Expand All @@ -24,7 +25,7 @@ def test_confirm_invite_no_token(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "404 NOT FOUND"
assert response.status_code == http.HTTPStatus.NOT_FOUND


def test_confirm_invite_invalid_token(client):
Expand All @@ -35,7 +36,7 @@ def test_confirm_invite_invalid_token(client):
headers=tests.DEFAULT_HEADER,
)

assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
# index redirects to login
assert flask.request.path == flask.url_for("pages.home")

Expand All @@ -56,7 +57,7 @@ def test_confirm_invite_expired_token(client):
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
# index redirects to login
assert flask.request.path == flask.url_for("pages.home")

Expand All @@ -75,7 +76,7 @@ def test_confirm_invite_valid_token(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert b"Create account" in response.data


Expand All @@ -86,7 +87,7 @@ def test_register_no_form(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "400 BAD REQUEST"
assert response.status_code == http.HTTPStatus.BAD_REQUEST


@pytest.fixture()
Expand All @@ -106,7 +107,7 @@ def registry_form_data(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert b"Create account" in response.data

form_token = flask.g.csrf_token
Expand All @@ -133,7 +134,7 @@ def test_register_no_token_in_session(registry_form_data, client):
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert flask.request.path == tests.DDSEndpoint.INDEX

# Invite should be kept and user should not be created
Expand All @@ -158,7 +159,7 @@ def test_register_weak_password(registry_form_data, client):
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert flask.request.path == tests.DDSEndpoint.USER_NEW

# Invite should be kept and user should not be created
Expand All @@ -172,14 +173,74 @@ def test_register_weak_password(registry_form_data, client):
assert user is None


def test_register_nonlatin1_username(registry_form_data, client):
"""Username can only contain latin 1 encodable characters.

Requests package does not allow non-latin1 encodable characters.
"""
form_data = registry_form_data

form_data["username"] = "user_€" # € is invalid

# Request should work
response = client.post(
tests.DDSEndpoint.USER_NEW,
json=form_data,
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status_code == http.HTTPStatus.OK
assert flask.request.path == tests.DDSEndpoint.USER_NEW

# Invite should be kept and user should not be created
invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
).one_or_none()
assert invite is not None

# No user should be created
user = models.User.query.filter_by(username=form_data["username"]).one_or_none()
assert user is None


def test_register_nonlatin1_password(registry_form_data, client):
"""Password can only contain latin 1 encodable characters.

Requests package does not allow non-latin1 encodable characters.
"""
form_data = registry_form_data
form_data["password"] = "Password123€" # € is invalid
form_data["confirm"] = "Password123€"

# Request should work
response = client.post(
tests.DDSEndpoint.USER_NEW,
json=form_data,
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status_code == http.HTTPStatus.OK
assert flask.request.path == tests.DDSEndpoint.USER_NEW

# Invite should be kept and user should not be created
invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
).one_or_none()
assert invite is not None

# No user should be created
user = models.User.query.filter_by(username=form_data["username"]).one_or_none()
assert user is None


def test_successful_registration(registry_form_data, client):
response = client.post(
tests.DDSEndpoint.USER_NEW,
json=registry_form_data,
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
Expand Down Expand Up @@ -211,7 +272,7 @@ def test_successful_registration_should_transfer_keys(registry_form_data, client
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
Expand Down Expand Up @@ -245,7 +306,7 @@ def test_invite_key_verification_fails_with_no_setup(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert b"Create account" in response.data

form_token = flask.g.csrf_token
Expand All @@ -266,7 +327,7 @@ def test_invite_key_verification_fails_with_no_setup(client):
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
Expand Down Expand Up @@ -302,7 +363,7 @@ def test_invite_key_verification_fails_with_wrong_valid_key(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert b"Create account" in response.data

form_token = flask.g.csrf_token
Expand All @@ -323,7 +384,7 @@ def test_invite_key_verification_fails_with_wrong_valid_key(client):
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
Expand Down Expand Up @@ -359,7 +420,7 @@ def test_invite_key_verification_fails_with_wrong_invalid_key(client):
content_type="application/json",
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK
assert b"Create account" in response.data

form_token = flask.g.csrf_token
Expand All @@ -380,7 +441,7 @@ def test_invite_key_verification_fails_with_wrong_invalid_key(client):
follow_redirects=True,
headers=tests.DEFAULT_HEADER,
)
assert response.status == "200 OK"
assert response.status_code == http.HTTPStatus.OK

invite = models.Invite.query.filter_by(
email="[email protected]", role="Researcher"
Expand Down
Loading