Skip to content

Commit

Permalink
Added journalist password change API
Browse files Browse the repository at this point in the history
  • Loading branch information
mdrose committed Oct 14, 2018
1 parent 6539cf6 commit a49f0b3
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 3 deletions.
23 changes: 22 additions & 1 deletion securedrop/journalist_app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from journalist_app import utils
from models import (Journalist, Reply, Source, Submission,
LoginThrottledException, InvalidUsernameException,
BadTokenException, WrongPasswordException)
BadTokenException, WrongPasswordException,
InvalidPasswordLength, NonDicewarePassword)
from store import NotEncrypted


Expand Down Expand Up @@ -279,6 +280,26 @@ def get_current_user():
user = get_user_object(request)
return jsonify(user.to_json()), 200

@api.route('/account/new-password', methods=['POST'])
@token_required
def new_password():
user = get_user_object(request)
new_password = json.loads(request.data)['new_password']

try:
user.set_password(new_password)
except (InvalidPasswordLength, NonDicewarePassword) as e:
return jsonify({'message': str(e)}), 400
except Exception as e:
return jsonify({'message': 'An error occurred while setting the password. Password unchanged'}), 500

try:
db.session.commit()
except Exception as e:
return jsonify({'message': 'An error occurred on database commit. Password unchanged.'}), 500

return jsonify({'message': 'Password changed successfully'}), 200

def _handle_http_exception(error):
# Workaround for no blueprint-level 404/5 error handlers, see:
# https://github.com/pallets/flask/issues/503#issuecomment-71383286
Expand Down
6 changes: 5 additions & 1 deletion securedrop/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ class NonDicewarePassword(PasswordError):

"""Raised when attempting to validate a password that is not diceware-like
"""
def __str__(self):
return ("Password needs to be a passphrase of at least "
"{} words.").format(Journalist.MIN_PASSWORD_WORDS)


class Journalist(db.Model):
Expand Down Expand Up @@ -361,6 +364,7 @@ def _scrypt_hash(self, password, salt):

MAX_PASSWORD_LEN = 128
MIN_PASSWORD_LEN = 14
MIN_PASSWORD_WORDS = 7

def set_password(self, passphrase):
self.check_password_acceptable(passphrase)
Expand Down Expand Up @@ -398,7 +402,7 @@ def check_password_acceptable(cls, password):
raise InvalidPasswordLength(password)

# Ensure all passwords are "diceware-like"
if len(password.split()) < 7:
if len(password.split()) < cls.MIN_PASSWORD_WORDS:
raise NonDicewarePassword()

def valid_password(self, passphrase):
Expand Down
43 changes: 42 additions & 1 deletion securedrop/tests/test_journalist_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from itsdangerous import TimedJSONWebSignatureSerializer

from db import db
from models import Journalist, Reply, Source, SourceStar, Submission
from models import (Journalist, Reply, Source, SourceStar, Submission,
NonDicewarePassword, InvalidPasswordLength)

os.environ['SECUREDROP_ENV'] = 'test' # noqa
from utils.api_helper import get_api_headers
Expand Down Expand Up @@ -662,6 +663,46 @@ def test_authorized_user_can_add_reply(journalist_app, journalist_api_token,
assert reply_content == saved_content


def test_new_password_unacceptable_400(journalist_app, journalist_api_token,
test_journo):
original_hash = test_journo['journalist'].passphrase_hash

with journalist_app.test_client() as app:
new_password = 'a' * (Journalist.MIN_PASSWORD_LEN - 1)
data = {'new_password': new_password}
response = app.post(url_for('api.new_password'),
data=json.dumps(data),
headers=get_api_headers(journalist_api_token))

assert response.status_code == 400
assert (response.get_json()['message'] == str(NonDicewarePassword()) or
response.get_json()['message'] == str(InvalidPasswordLength(new_password))
)

with journalist_app.app_context():
user = Journalist.query.get(test_journo['id'])
assert original_hash == user.passphrase_hash


def test_new_password_success_200(journalist_app, journalist_api_token,
test_journo):
original_hash = test_journo['journalist'].passphrase_hash

with journalist_app.test_client() as app:
new_password = 'a ' * max( (Journalist.MIN_PASSWORD_LEN+1) / 2,
Journalist.MIN_PASSWORD_WORDS)

data = {'new_password': new_password}
response = app.post(url_for('api.new_password'),
data=json.dumps(data),
headers=get_api_headers(journalist_api_token))
assert response.status_code == 200

with journalist_app.app_context():
user = Journalist.query.get(test_journo['id'])
assert original_hash != user.passphrase_hash


def test_reply_without_content_400(journalist_app, journalist_api_token,
test_source, test_journo):
with journalist_app.test_client() as app:
Expand Down

0 comments on commit a49f0b3

Please sign in to comment.