about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSam2013-10-15 02:50:25 -0500
committerSam2013-10-15 02:50:25 -0500
commit57b00317168eb3a84c489c2613133170a191aef6 (patch)
tree88bc4cbe70b1199fb80021d7213971bdec9674bd
parent34c6c908ac072609a2f923946d474504d3fa0331 (diff)
downloadgenenetwork2-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
-rw-r--r--wqflask/secure_server.py40
-rw-r--r--wqflask/wqflask/send_mail.py39
-rw-r--r--wqflask/wqflask/templates/email/forgot_password.txt5
-rw-r--r--wqflask/wqflask/templates/email/verification.txt3
-rw-r--r--wqflask/wqflask/templates/new_security/forgot_password.html65
-rw-r--r--wqflask/wqflask/templates/new_security/login_user.html55
-rw-r--r--wqflask/wqflask/user_manager.py165
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 =