diff options
author | Sam | 2013-10-15 02:50:25 -0500 |
---|---|---|
committer | Sam | 2013-10-15 02:50:25 -0500 |
commit | 57b00317168eb3a84c489c2613133170a191aef6 (patch) | |
tree | 88bc4cbe70b1199fb80021d7213971bdec9674bd /wqflask | |
parent | 34c6c908ac072609a2f923946d474504d3fa0331 (diff) | |
download | genenetwork2-57b00317168eb3a84c489c2613133170a191aef6.tar.gz |
* Redid mail sending service to set a ping showing it's alive
* Redid server so it wouldn't start unless mail service recently pinged
* Wrote a bunch of forgot password code
* Refactored sending email about verification and forgotten passwords
Diffstat (limited to 'wqflask')
-rw-r--r-- | wqflask/secure_server.py | 40 | ||||
-rw-r--r-- | wqflask/wqflask/send_mail.py | 39 | ||||
-rw-r--r-- | wqflask/wqflask/templates/email/forgot_password.txt | 5 | ||||
-rw-r--r-- | wqflask/wqflask/templates/email/verification.txt | 3 | ||||
-rw-r--r-- | wqflask/wqflask/templates/new_security/forgot_password.html | 65 | ||||
-rw-r--r-- | wqflask/wqflask/templates/new_security/login_user.html | 55 | ||||
-rw-r--r-- | wqflask/wqflask/user_manager.py | 165 |
7 files changed, 258 insertions, 114 deletions
diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index a77abf7e..d5f1a291 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -1,12 +1,18 @@ from __future__ import absolute_import, division, print_function +import time +import sys + from wqflask import app from flask import Flask, render_template +import redis +Redis = redis.StrictRedis() + # Setup mail -from flask.ext.mail import Mail -mail = Mail(app) +#from flask.ext.mail import Mail +#mail = Mail(app) from wqflask.model import * @@ -33,8 +39,38 @@ app.wsgi_app = ProxyFix(app.wsgi_app) #print("app.config is:", app.config) + + +def check_send_mail_running(): + """Ensure send_mail.py is running before we start the site + + It would be really easy to accidentally run the site + without our mail program running + This will make sure our mail program is running...or at least recently run... + + """ + error_msg = "Make sure your are running send_mail.py" + send_mail_ping = Redis.get("send_mail:ping") + print("send_mail_ping is:", send_mail_ping) + if not send_mail_ping: + sys.exit(error_msg) + + last_ping = time.time() - float(send_mail_ping) + if not (0 < last_ping < 100): + sys.exit(error_msg) + + + print("send_mail.py seems to be running...") + + if __name__ == '__main__': #create_user() + + + + check_send_mail_running() + + app.run(host='0.0.0.0', port=app.config['SERVER_PORT'], use_debugger=False, diff --git a/wqflask/wqflask/send_mail.py b/wqflask/wqflask/send_mail.py index be51ad0d..bf5d0dd8 100644 --- a/wqflask/wqflask/send_mail.py +++ b/wqflask/wqflask/send_mail.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, division, print_function import datetime +import time import simplejson as json @@ -12,34 +13,40 @@ import mailer def timestamp(): ts = datetime.datetime.utcnow() return ts.isoformat() - + def main(): while True: - print("Waiting for message to show up in queue...") - msg = Redis.blpop("mail_queue") - - # Queue name is the first element, we want the second, which is the actual message - msg = msg[1] - - print("\nGot a msg in queue at {}: {}".format(timestamp(), msg)) - # Todo: Truncate mail_processed when it gets to long - Redis.rpush("mail_processed", msg) - process_message(msg) - + print("I'm alive!") + + # Set something so we know it's running (or at least been running recently) + Redis.setex("send_mail:ping", 300, time.time()) + + msg = Redis.blpop("mail_queue", 30) + + if msg: + # Queue name is the first element, we want the second, which is the actual message + msg = msg[1] + + print("\n\nGot a msg in queue at {}: {}".format(timestamp(), msg)) + # Todo: Truncate mail_processed when it gets to long + Redis.rpush("mail_processed", msg) + process_message(msg) + + def process_message(msg): msg = json.loads(msg) - + message = mailer.Message() message.From = msg['From'] message.To = msg['To'] message.Subject = msg['Subject'] message.Body = msg['Body'] - + sender = mailer.Mailer('localhost') sender.send(message) print("Sent message at {}: {}\n".format(timestamp(), msg)) - + if __name__ == '__main__': - main()
\ No newline at end of file + main() diff --git a/wqflask/wqflask/templates/email/forgot_password.txt b/wqflask/wqflask/templates/email/forgot_password.txt new file mode 100644 index 00000000..e7d1389b --- /dev/null +++ b/wqflask/wqflask/templates/email/forgot_password.txt @@ -0,0 +1,5 @@ +Sorry to hear you lost your GeneNetwork password. + +To reset your password please click the following link, or cut and paste it into your browser window: + +{{ url_for_hmac("password_reset", code = verification_code, _external=True )}} diff --git a/wqflask/wqflask/templates/email/verification.txt b/wqflask/wqflask/templates/email/verification.txt index 29229c68..76149a3a 100644 --- a/wqflask/wqflask/templates/email/verification.txt +++ b/wqflask/wqflask/templates/email/verification.txt @@ -4,5 +4,4 @@ We need to verify your email address. To do that please click the following link, or cut and paste it into your browser window: -{{ url_for_hmac("verify", code = verification_code, _external=True )}} - +{{ url_for_hmac("verify_email", code = verification_code, _external=True )}} diff --git a/wqflask/wqflask/templates/new_security/forgot_password.html b/wqflask/wqflask/templates/new_security/forgot_password.html new file mode 100644 index 00000000..39e51f96 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/forgot_password.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block title %}Forgot Password{% endblock %} +{% block content %} + + {{ header("Forgot Password", "Easily reset your password.") }} + + <div class="container"> + <div class="page-header"> + <h1>Password Reset</h1> + </div> + + + <div class="security_box"> + + <h4>Enter your email address</h4> + + <h5>And we'll send you a link to reset your password</h5> + + + + <form class="form-horizontal" action="/n/forgot_password_submit" + method="POST" name="login_user_form"> + <fieldset> + + + <div class="control-group"> + <label class="control-label" for="email_address">Email Address</label> + <div class="controls"> + <input id="email_address" class="focused" name="email_address" type="text" value=""> + </div> + </div> + + + <div class="control-group"> + <div class="controls"> + <input id="next" name="next" type="hidden" value=""> + + <input class="btn btn-primary" id="submit" name="submit" type="submit" value="Send link"> + </div> + </div> + + </fieldset> + + <br /> + + <div class="well"> + <h5>Has your email address changed?</h5> + + If you no longer use the email address connected to your account, you can contact us for assistance. + + </div> + + </form> + </div> + </div> + </div> + + {% endblock %} + +{% block js %} + <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> + + {% include "new_security/_scripts.html" %} + +{% endblock %} diff --git a/wqflask/wqflask/templates/new_security/login_user.html b/wqflask/wqflask/templates/new_security/login_user.html index f232ccbc..4e308c75 100644 --- a/wqflask/wqflask/templates/new_security/login_user.html +++ b/wqflask/wqflask/templates/new_security/login_user.html @@ -2,82 +2,81 @@ {% block title %}Register{% endblock %} {% block content %} - {{ header("Login", "Gain access to GeneNetwork.") }} + {{ header("Sign in", "Gain access to GeneNetwork") }} <div class="container"> <div class="page-header"> <h1>Login</h1> </div> - + <div class="security_box"> - + <h4>Don't have an account?</h4> - - + + <a href="/n/register" class="btn btn-info modalize">Create a new account</a> - - + + <hr /> - + <h4>Already have an account?</h4> - + <h5>Sign in here</h5> - - - + + + <form class="form-horizontal" action="/n/login" method="POST" name="login_user_form"> <fieldset> - - + + <div class="control-group"> <label class="control-label" for="email_address">Email Address</label> <div class="controls"> <input id="email_address" class="focused" name="email_address" type="text" value=""> </div> </div> - + <div class="control-group"> <label class="control-label" for="password">Password</label> <div class="controls"> <input id="password" name="password" type="password" value=""> <br /> - <a href="url_for_security('forgot_password')">Forgot your password?</a><br/> + <a href="/n/forgot_password">Forgot your password?</a><br/> </div> </div> - - + + <div class="control-group"> <div class="controls"> <label class="checkbox"> <input id="remember" name="remember" type="checkbox" value="y"> Remember me </label> </div> - - + + <div class="control-group"> <div class="controls"> <input id="next" name="next" type="hidden" value=""> - + <input class="btn btn-primary" id="submit" name="submit" type="submit" value="Sign in"> </div> - - + + </div> </fieldset> - + </form> </div> </div> </div> - + {% endblock %} -{% block js %} +{% block js %} <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> - + {% include "new_security/_scripts.html" %} {% endblock %} - diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index a2dff7f2..1df2c153 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -19,8 +19,11 @@ import base64 import simplejson as json -from redis import StrictRedis -Redis = StrictRedis() +from sqlalchemy import orm + +#from redis import StrictRedis +import redis +Redis = redis.StrictRedis() from flask import (Flask, g, render_template, url_for, request, make_response, @@ -53,7 +56,7 @@ def timestamp(): class UserSession(object): cookie_name = 'session_id' - + def __init__(self): cookie = request.cookies.get(self.cookie_name) if not cookie: @@ -70,13 +73,13 @@ class UserSession(object): self.record = Redis.hgetall(self.redis_key) print("record is:", self.record) self.logged_in = True - - + + def delete_session(self): # And more importantly delete the redis record Redis.delete(self.cookie_name) print("At end of delete_session") - + @app.before_request def before_request(): g.user_session = UserSession() @@ -105,22 +108,22 @@ class UserManager(object): print(" ID:", dataset.id) print(" Confidential:", dataset.check_confidentiality()) #print(" ---> self.datasets:", self.datasets) - + class RegisterUser(object): def __init__(self, kw): self.thank_you_mode = False self.errors = [] self.user = Bunch() - + self.user.email_address = kw.get('email_address', '').strip() if not (5 <= len(self.user.email_address) <= 50): self.errors.append('Email Address needs to be between 5 and 50 characters.') - + self.user.full_name = kw.get('full_name', '').strip() if not (5 <= len(self.user.full_name) <= 50): self.errors.append('Full Name needs to be between 5 and 50 characters.') - + self.user.organization = kw.get('organization', '').strip() if self.user.organization and not (5 <= len(self.user.organization) <= 50): self.errors.append('Organization needs to be empty or between 5 and 50 characters.') @@ -128,48 +131,49 @@ class RegisterUser(object): password = str(kw.get('password', '')) if not (6 <= len(password)): self.errors.append('Password needs to be at least 6 characters.') - + if kw.get('password_confirm') != password: self.errors.append("Passwords don't match.") - + if self.errors: return - + print("No errors!") - + self.set_password(password) - + self.user.registration_info = json.dumps(basic_info(), sort_keys=True) - + self.new_user = model.User(**self.user.__dict__) db_session.add(self.new_user) db_session.commit() - + print("Adding verification email to queue") - self.send_email_verification() + #self.send_email_verification() + VerificationEmail(self.new_user) print("Added verification email to queue") - + self.thank_you_mode = True - - + + def set_password(self, password): pwfields = Bunch() - + pwfields.algorithm = "pbkdf2" pwfields.hashfunc = "sha256" #hashfunc = getattr(hashlib, pwfields.hashfunc) - - # Encoding it to base64 makes storing it in json much easier + + # Encoding it to base64 makes storing it in json much easier pwfields.salt = base64.b64encode(os.urandom(32)) # https://forums.lastpass.com/viewtopic.php?t=84104 - pwfields.iterations = 100000 + pwfields.iterations = 100000 pwfields.keylength = 32 - + pwfields.created_ts = timestamp() # One more check on password length assert len(password) >= 6, "Password shouldn't be so short here" - + print("pwfields:", vars(pwfields)) print("locals:", locals()) @@ -178,31 +182,42 @@ class RegisterUser(object): pwfields.iterations, pwfields.keylength, pwfields.hashfunc) - + pwfields.password = enc_password.password pwfields.encrypt_time = enc_password.encrypt_time - + self.user.password = json.dumps(pwfields.__dict__, - sort_keys=True, + sort_keys=True, ) - - def send_email_verification(self): + +class VerificationEmail(object): + template_name = "email/verification.txt" + key_preface = "verification_code" + subject = "GeneNetwork email verification" + + def __init__(self, user): verification_code = str(uuid.uuid4()) - key = "verification_code:" + verification_code - - data = json.dumps(dict(id=self.new_user.id, + key = self.key_preface + "verification_code:" + verification_code + + data = json.dumps(dict(id=user.id, timestamp=timestamp()) ) - + Redis.set(key, data) two_days = 60 * 60 * 24 * 2 - Redis.expire(key, two_days) - to = self.user.email_address - subject = "GeneNetwork email verification" - body = render_template("email/verification.txt", + Redis.expire(key, two_days) + to = user.email_address + subject = self.subject + body = render_template(self.template_name, verification_code = verification_code) send_email(to, subject, body) +class ForgotPasswordEmail(VerificationEmail): + template_name = "email/forgot_password.txt" + key_preface = "forgot_password_code" + subject = "GeneNetwork password reset" + + class Password(object): def __init__(self, unencrypted_password, salt, iterations, keylength, hashfunc): hashfunc = getattr(hashlib, hashfunc) @@ -215,13 +230,14 @@ class Password(object): salt, iterations, keylength, hashfunc) self.encrypt_time = round(time.time() - start_time, 3) print("Creating password took:", self.encrypt_time) - - + + def basic_info(): return dict(timestamp = timestamp(), ip_address = request.remote_addr, user_agent = request.headers.get('User-Agent')) +@app.route("/manage/verify_email") def verify_email(): print("in verify_email request.url is:", request.url) verify_url_hmac(request.url) @@ -232,7 +248,7 @@ def verify_email(): user = model.User.query.get(data['id']) user.confirmed = json.dumps(basic_info(), sort_keys=True) db_session.commit() - + def login(): params = request.form if request.form else request.args print("in login params are:", params) @@ -250,10 +266,10 @@ def login(): print("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password)) valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password) print("valid is:", valid) - + login_rec = model.Login(user) - - + + if valid: login_rec.successful = True login_rec.session_id = str(uuid.uuid4()) @@ -261,17 +277,17 @@ def login(): session_id_signature = actual_hmac_creation(login_rec.session_id) session_id_signed = login_rec.session_id + ":" + session_id_signature print("session_id_signed:", session_id_signed) - + session = dict(login_time = time.time(), user_id = user.id, user_email_address = user.email_address) - + flash("Thank you for logging in.", "alert-success") - + key = "session_id:" + login_rec.session_id print("Key when signing:", key) Redis.hmset(key, session) - + response = make_response(redirect(url_for('index_page'))) response.set_cookie(UserSession.cookie_name, session_id_signed) else: @@ -281,8 +297,8 @@ def login(): db_session.add(login_rec) db_session.commit() return response - -@app.route("/n/logout") + +@app.route("/n/logout") def logout(): print("Logging out...") UserSession().delete_session() @@ -291,15 +307,36 @@ def logout(): # Delete the cookie response.set_cookie(UserSession.cookie_name, '', expires=0) return response - - + + +@app.route("/n/forgot_password") +def forgot_password(): + return render_template("new_security/forgot_password.html") + +@app.route("/n/forgot_password_submit", methods=('POST',)) +def forgot_password_submit(): + params = request.form + email_address = params['email_address'] + try: + user = model.User.query.filter_by(email_address=email_address).one() + except orm.exc.NoResultFound: + flash("Couldn't find a user associated with the email address {}. Sorry.".format( + email_address)) + return redirect(url_for("login")) + ForgotPasswordEmail(user) + +@app.route("/n/password_reset") +def password_reset(): + pass + + ################################# Sign and unsign ##################################### def url_for_hmac(endpoint, **values): """Like url_for but adds an hmac at the end to insure the url hasn't been tampered with""" url = url_for(endpoint, **values) - + hm = actual_hmac_creation(url) if '?' in url: combiner = "&" @@ -323,10 +360,10 @@ def verify_url_hmac(url): hm = actual_hmac_creation(url) assert hm == hmac, "Unexpected url (stage 3)" - + def actual_hmac_creation(stringy): """Helper function to create the actual hmac""" - + secret = app.config['SECRET_HMAC_CODE'] hmaced = hmac.new(secret, stringy, hashlib.sha1) @@ -339,14 +376,14 @@ def actual_hmac_creation(stringy): app.jinja_env.globals.update(url_for_hmac=url_for_hmac) ####################################################################################### - + def send_email(to, subject, body): msg = json.dumps(dict(From="no-reply@genenetwork.org", To=to, Subject=subject, Body=body)) Redis.rpush("mail_queue", msg) - + #def combined_salt(user_salt): # """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the @@ -358,7 +395,7 @@ def send_email(to, subject, body): # for x, y in user_salt, secret_salt: # combined = combined + x + y # return combined - + class GroupsManager(object): @@ -374,9 +411,5 @@ class RolesManager(object): #class Password(object): # """To generate a master password: dd if=/dev/urandom bs=32 count=1 > master_salt""" -# -# master_salt = - - - - +# +# master_salt = |