aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 =