From 69a1bef6f0e653e0a8d84e4e13e5879e2faeea39 Mon Sep 17 00:00:00 2001 From: zsloan Date: Tue, 3 Mar 2020 15:50:17 -0600 Subject: I made a pretty big mistake removing this file; I forgot that it was still used for some collection stuff --- wqflask/wqflask/user_manager.py | 1058 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1058 insertions(+) create mode 100644 wqflask/wqflask/user_manager.py (limited to 'wqflask') diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py new file mode 100644 index 00000000..1b27d7cb --- /dev/null +++ b/wqflask/wqflask/user_manager.py @@ -0,0 +1,1058 @@ +from __future__ import print_function, division, absolute_import + +import os +import hashlib +import datetime +import time +import logging +import uuid +import hashlib +import hmac +import base64 +import urlparse + +import simplejson as json + +#from redis import StrictRedis +import redis # used for collections +Redis = redis.StrictRedis() + +from flask import (Flask, g, render_template, url_for, request, make_response, + redirect, flash, abort) + +from wqflask import app +from pprint import pformat as pf + +from wqflask import pbkdf2 # password hashing +from wqflask.database import db_session +from wqflask import model + +from utility import Bunch, Struct, after + +import logging +from utility.logger import getLogger +logger = getLogger(__name__) + +from base.data_set import create_datasets_list + +import requests + +from utility.redis_tools import get_user_id, get_user_by_unique_column, set_user_attribute, save_user, save_verification_code, check_verification_code, get_user_collections, save_collections + +from smtplib import SMTP +from utility.tools import SMTP_CONNECT, SMTP_USERNAME, SMTP_PASSWORD, LOG_SQL_ALCHEMY + +THREE_DAYS = 60 * 60 * 24 * 3 +#THREE_DAYS = 45 + +def timestamp(): + return datetime.datetime.utcnow().isoformat() + +class AnonUser(object): + """Anonymous user handling""" + cookie_name = 'anon_user_v1' + + def __init__(self): + self.cookie = request.cookies.get(self.cookie_name) + if self.cookie: + logger.debug("ANON COOKIE ALREADY EXISTS") + self.anon_id = verify_cookie(self.cookie) + else: + logger.debug("CREATING NEW ANON COOKIE") + self.anon_id, self.cookie = create_signed_cookie() + + self.key = "anon_collection:v1:{}".format(self.anon_id) + + def add_collection(self, new_collection): + collection_dict = dict(name = new_collection.name, + created_timestamp = datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p'), + changed_timestamp = datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p'), + num_members = new_collection.num_members, + members = new_collection.get_members()) + + Redis.set(self.key, json.dumps(collection_dict)) + Redis.expire(self.key, 60 * 60 * 24 * 365) + + def delete_collection(self, collection_name): + existing_collections = self.get_collections() + updated_collections = [] + for i, collection in enumerate(existing_collections): + if collection['name'] == collection_name: + continue + else: + this_collection = {} + this_collection['id'] = collection['id'] + this_collection['name'] = collection['name'] + this_collection['created_timestamp'] = collection['created_timestamp'].strftime('%b %d %Y %I:%M%p') + this_collection['changed_timestamp'] = collection['changed_timestamp'].strftime('%b %d %Y %I:%M%p') + this_collection['num_members'] = collection['num_members'] + this_collection['members'] = collection['members'] + updated_collections.append(this_collection) + + Redis.set(self.key, json.dumps(updated_collections)) + + def get_collections(self): + json_collections = Redis.get(self.key) + if json_collections == None or json_collections == "None": + return [] + else: + collections = json.loads(json_collections) + for collection in collections: + collection['created_timestamp'] = datetime.datetime.strptime(collection['created_timestamp'], '%b %d %Y %I:%M%p') + collection['changed_timestamp'] = datetime.datetime.strptime(collection['changed_timestamp'], '%b %d %Y %I:%M%p') + + collections = sorted(collections, key = lambda i: i['changed_timestamp'], reverse = True) + return collections + + def import_traits_to_user(self): + result = Redis.get(self.key) + collections_list = json.loads(result if result else "[]") + for collection in collections_list: + collection_exists = g.user_session.get_collection_by_name(collection['name']) + if collection_exists: + continue + else: + g.user_session.add_collection(collection['name'], collection['members']) + + def display_num_collections(self): + """ + Returns the number of collections or a blank string if there are zero. + + Because this is so unimportant...we wrap the whole thing in a try/expect...last thing we + want is a webpage not to be displayed because of an error here + + Importand TODO: use redis to cache this, don't want to be constantly computing it + """ + try: + num = len(self.get_collections()) + if num > 0: + return num + else: + return "" + except Exception as why: + print("Couldn't display_num_collections:", why) + return "" + + +def verify_cookie(cookie): + the_uuid, separator, the_signature = cookie.partition(':') + assert len(the_uuid) == 36, "Is session_id a uuid?" + assert separator == ":", "Expected a : here" + assert the_signature == actual_hmac_creation(the_uuid), "Uh-oh, someone tampering with the cookie?" + return the_uuid + +def create_signed_cookie(): + the_uuid = str(uuid.uuid4()) + signature = actual_hmac_creation(the_uuid) + uuid_signed = the_uuid + ":" + signature + logger.debug("uuid_signed:", uuid_signed) + return the_uuid, uuid_signed + +class UserSession(object): + """Logged in user handling""" + + cookie_name = 'session_id_v1' + + def __init__(self): + cookie = request.cookies.get(self.cookie_name) + if not cookie: + logger.debug("NO USER COOKIE") + self.logged_in = False + return + else: + session_id = verify_cookie(cookie) + + self.redis_key = self.cookie_name + ":" + session_id + logger.debug("self.redis_key is:", self.redis_key) + self.session_id = session_id + self.record = Redis.hgetall(self.redis_key) + + if not self.record: + # This will occur, for example, when the browser has been left open over a long + # weekend and the site hasn't been visited by the user + self.logged_in = False + + ########### Grrr...this won't work because of the way flask handles cookies + # Delete the cookie + #response = make_response(redirect(url_for('login'))) + #response.set_cookie(self.cookie_name, '', expires=0) + #flash( + # "Due to inactivity your session has expired. If you'd like please login again.") + #return response + return + + if Redis.ttl(self.redis_key) < THREE_DAYS: + # (Almost) everytime the user does something we extend the session_id in Redis... + logger.debug("Extending ttl...") + Redis.expire(self.redis_key, THREE_DAYS) + + logger.debug("record is:", self.record) + self.logged_in = True + + @property + def user_id(self): + """Shortcut to the user_id""" + if 'user_id' in self.record: + return self.record['user_id'] + else: + return '' + + @property + def redis_user_id(self): + """User id from ElasticSearch (need to check if this is the same as the id stored in self.records)""" + + user_email = self.record['user_email_address'] + + #ZS: Get user's collections if they exist + user_id = None + user_id = get_user_id("email_address", user_email) + return user_id + + @property + def user_name(self): + """Shortcut to the user_name""" + if 'user_name' in self.record: + return self.record['user_name'] + else: + return '' + + @property + def user_collections(self): + """List of user's collections""" + + #ZS: Get user's collections if they exist + collections = get_user_collections(self.redis_user_id) + return collections + + @property + def num_collections(self): + """Number of user's collections""" + + return len(self.user_collections) + +### +# ZS: This is currently not used, but I'm leaving it here commented out because the old "set superuser" code (at the bottom of this file) used it +### +# @property +# def user_ob(self): +# """Actual sqlalchemy record""" +# # Only look it up once if needed, then store it +# # raise "OBSOLETE: use ElasticSearch instead" +# try: +# if LOG_SQL_ALCHEMY: +# logging.getLogger('sqlalchemy.pool').setLevel(logging.DEBUG) +# +# # Already did this before +# return self.db_object +# except AttributeError: +# # Doesn't exist so we'll create it +# self.db_object = model.User.query.get(self.user_id) +# return self.db_object + + def add_collection(self, collection_name, traits): + """Add collection into ElasticSearch""" + + collection_dict = {'id': unicode(uuid.uuid4()), + 'name': collection_name, + 'created_timestamp': datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p'), + 'changed_timestamp': datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p'), + 'num_members': len(traits), + 'members': list(traits) } + + current_collections = self.user_collections + current_collections.append(collection_dict) + self.update_collections(current_collections) + + return collection_dict['id'] + + def delete_collection(self, collection_id): + """Remove collection with given ID""" + + updated_collections = [] + for collection in self.user_collections: + if collection['id'] == collection_id: + continue + else: + updated_collections.append(collection) + + self.update_collections(updated_collections) + + return collection['name'] + + def add_traits_to_collection(self, collection_id, traits_to_add): + """Add specified traits to a collection""" + + this_collection = self.get_collection_by_id(collection_id) + + updated_collection = this_collection + updated_traits = this_collection['members'] + traits_to_add + + updated_collection['members'] = updated_traits + updated_collection['num_members'] = len(updated_traits) + updated_collection['changed_timestamp'] = datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p') + + updated_collections = [] + for collection in self.user_collections: + if collection['id'] == collection_id: + updated_collections.append(updated_collection) + else: + updated_collections.append(collection) + + self.update_collections(updated_collections) + + def remove_traits_from_collection(self, collection_id, traits_to_remove): + """Remove specified traits from a collection""" + + this_collection = self.get_collection_by_id(collection_id) + + updated_collection = this_collection + updated_traits = [] + for trait in this_collection['members']: + if trait in traits_to_remove: + continue + else: + updated_traits.append(trait) + + updated_collection['members'] = updated_traits + updated_collection['num_members'] = len(updated_traits) + updated_collection['changed_timestamp'] = datetime.datetime.utcnow().strftime('%b %d %Y %I:%M%p') + + updated_collections = [] + for collection in self.user_collections: + if collection['id'] == collection_id: + updated_collections.append(updated_collection) + else: + updated_collections.append(collection) + + self.update_collections(updated_collections) + + return updated_traits + + def get_collection_by_id(self, collection_id): + for collection in self.user_collections: + if collection['id'] == collection_id: + return collection + + def get_collection_by_name(self, collection_name): + for collection in self.user_collections: + if collection['name'] == collection_name: + return collection + + return None + + def update_collections(self, updated_collections): + collection_body = json.dumps(updated_collections) + + save_collections(self.redis_user_id, collection_body) + + def delete_session(self): + # And more importantly delete the redis record + Redis.delete(self.cookie_name) + logger.debug("At end of delete_session") + +@app.before_request +def before_request(): + g.user_session = UserSession() + g.cookie_session = AnonUser() + +@app.after_request +def set_cookie(response): + if not request.cookies.get(g.cookie_session.cookie_name): + response.set_cookie(g.cookie_session.cookie_name, g.cookie_session.cookie) + return response + +class UsersManager(object): + def __init__(self): + self.users = model.User.query.all() + logger.debug("Users are:", self.users) + +class UserManager(object): + def __init__(self, kw): + self.user_id = kw['user_id'] + logger.debug("In UserManager locals are:", pf(locals())) + #self.user = model.User.get(user_id) + #logger.debug("user is:", user) + self.user = model.User.query.get(self.user_id) + logger.debug("user is:", self.user) + datasets = create_datasets_list() + for dataset in datasets: + if not dataset.check_confidentiality(): + continue + logger.debug("\n Name:", dataset.name) + logger.debug(" Type:", dataset.type) + logger.debug(" ID:", dataset.id) + logger.debug(" Confidential:", dataset.check_confidentiality()) + #logger.debug(" ---> 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', '').encode("utf-8").strip() + if not (5 <= len(self.user.email_address) <= 50): + self.errors.append('Email Address needs to be between 5 and 50 characters.') + else: + email_exists = get_user_by_unique_column("email_address", self.user.email_address) + #email_exists = get_user_by_unique_column(es, "email_address", self.user.email_address) + if email_exists: + self.errors.append('User already exists with that email') + + self.user.full_name = kw.get('full_name', '').encode("utf-8").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', '').encode("utf-8").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.') + + 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 + + logger.debug("No errors!") + + set_password(password, self.user) + self.user.user_id = str(uuid.uuid4()) + self.user.confirmed = 1 + + self.user.registration_info = json.dumps(basic_info(), sort_keys=True) + save_user(self.user.__dict__, self.user.user_id) + +def set_password(password, user): + pwfields = Bunch() + + pwfields.algorithm = "pbkdf2" + pwfields.hashfunc = "sha256" + #hashfunc = getattr(hashlib, pwfields.hashfunc) + + # 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.keylength = 32 + + pwfields.created_ts = timestamp() + # One more check on password length + assert len(password) >= 6, "Password shouldn't be so short here" + + logger.debug("pwfields:", vars(pwfields)) + logger.debug("locals:", locals()) + + enc_password = Password(password, + pwfields.salt, + pwfields.iterations, + pwfields.keylength, + pwfields.hashfunc) + + pwfields.password = enc_password.password + pwfields.encrypt_time = enc_password.encrypt_time + + user.password = json.dumps(pwfields.__dict__, + sort_keys=True, + ) + + +class VerificationEmail(object): + template_name = "email/verification.txt" + key_prefix = "verification_code" + subject = "GeneNetwork email verification" + + def __init__(self, user): + verification_code = str(uuid.uuid4()) + key = self.key_prefix + ":" + verification_code + + data = json.dumps(dict(id=user.user_id, + timestamp=timestamp()) + ) + + Redis.set(key, data) + #two_days = 60 * 60 * 24 * 2 + Redis.expire(key, THREE_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_prefix = "forgot_password_code" + subject = "GeneNetwork password reset" + fromaddr = "no-reply@genenetwork.org" + + def __init__(self, toaddr): + from email.MIMEMultipart import MIMEMultipart + from email.MIMEText import MIMEText + verification_code = str(uuid.uuid4()) + key = self.key_prefix + ":" + verification_code + + data = { + "verification_code": verification_code, + "email_address": toaddr, + "timestamp": timestamp() + } + + save_verification_code(toaddr, verification_code) + + + subject = self.subject + body = render_template( + self.template_name, + verification_code = verification_code) + + msg = MIMEMultipart() + msg["To"] = toaddr + msg["Subject"] = self.subject + msg["From"] = self.fromaddr + msg.attach(MIMEText(body, "plain")) + + send_email(toaddr, msg.as_string()) + + +class Password(object): + def __init__(self, unencrypted_password, salt, iterations, keylength, hashfunc): + hashfunc = getattr(hashlib, hashfunc) + logger.debug("hashfunc is:", hashfunc) + # On our computer it takes around 1.4 seconds in 2013 + start_time = time.time() + salt = base64.b64decode(salt) + self.password = pbkdf2.pbkdf2_hex(str(unencrypted_password), + salt, iterations, keylength, hashfunc) + self.encrypt_time = round(time.time() - start_time, 3) + logger.debug("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(): + user = DecodeUser(VerificationEmail.key_prefix).user + user.confirmed = json.dumps(basic_info(), sort_keys=True) + db_session.commit() + + # As long as they have access to the email account + # We might as well log them in + + session_id_signed = LoginUser().successful_login(user) + response = make_response(render_template("new_security/thank_you.html")) + response.set_cookie(UserSession.cookie_name, session_id_signed) + return response + +@app.route("/n/password_reset", methods=['GET']) +def password_reset(): + """Entry point after user clicks link in E-mail""" + logger.debug("in password_reset request.url is:", request.url) + # We do this mainly just to assert that it's in proper form for displaying next page + # Really not necessary but doesn't hurt + # user_encode = DecodeUser(ForgotPasswordEmail.key_prefix).reencode_standalone() + verification_code = request.args.get('code') + hmac = request.args.get('hm') + + if verification_code: + user_email = check_verification_code(verification_code) + if user_email: + user_details = get_user_by_unique_column('email_address', user_email) + if user_details: + return render_template( + "new_security/password_reset.html", user_encode=user_details["user_id"]) + else: + flash("Invalid code: User no longer exists!", "error") + else: + flash("Invalid code: Password reset code does not exist or might have expired!", "error") + else: + return redirect(url_for("login")) + +@app.route("/n/password_reset_step2", methods=('POST',)) +def password_reset_step2(): + """Handle confirmation E-mail for password reset""" + logger.debug("in password_reset request.url is:", request.url) + + errors = [] + user_id = request.form['user_encode'] + + logger.debug("locals are:", locals()) + + + user = Bunch() + password = request.form['password'] + set_password(password, user) + + set_user_attribute(user_id, "password", user.__dict__.get("password")) + + flash("Password changed successfully. You can now sign in.", "alert-info") + response = make_response(redirect(url_for('login'))) + + return response + +class DecodeUser(object): + + def __init__(self, code_prefix): + verify_url_hmac(request.url) + + #params = urlparse.parse_qs(url) + + self.verification_code = request.args['code'] + self.user = self.actual_get_user(code_prefix, self.verification_code) + + def reencode_standalone(self): + hmac = actual_hmac_creation(self.verification_code) + return self.verification_code + ":" + hmac + + @staticmethod + def actual_get_user(code_prefix, verification_code): + data = Redis.get(code_prefix + ":" + verification_code) + logger.debug("in get_coded_user, data is:", data) + data = json.loads(data) + logger.debug("data is:", data) + return model.User.query.get(data['id']) + +@app.route("/n/login", methods=('GET', 'POST')) +def login(): + lu = LoginUser() + login_type = request.args.get("type") + if login_type: + uid = request.args.get("uid") + return lu.oauth2_login(login_type, uid) + else: + return lu.standard_login() + +@app.route("/n/login/github_oauth2", methods=('GET', 'POST')) +def github_oauth2(): + from utility.tools import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET + code = request.args.get("code") + data = { + "client_id": GITHUB_CLIENT_ID, + "client_secret": GITHUB_CLIENT_SECRET, + "code": code + } + result = requests.post("https://github.com/login/oauth/access_token", json=data) + result_dict = {arr[0]:arr[1] for arr in [tok.split("=") for tok in [token.encode("utf-8") for token in result.text.split("&")]]} + + github_user = get_github_user_details(result_dict["access_token"]) + + user_details = get_user_by_unique_column("github_id", github_user["id"]) + if user_details == None: + user_details = { + "user_id": str(uuid.uuid4()) + , "name": github_user["name"].encode("utf-8") + , "github_id": github_user["id"] + , "user_url": github_user["html_url"].encode("utf-8") + , "login_type": "github" + , "organization": "" + , "active": 1 + , "confirmed": 1 + } + save_user(user_details, user_details["user_id"]) + + url = "/n/login?type=github&uid="+user_details["user_id"] + return redirect(url) + +@app.route("/n/login/orcid_oauth2", methods=('GET', 'POST')) +def orcid_oauth2(): + from uuid import uuid4 + from utility.tools import ORCID_CLIENT_ID, ORCID_CLIENT_SECRET, ORCID_TOKEN_URL, ORCID_AUTH_URL + code = request.args.get("code") + error = request.args.get("error") + url = "/n/login" + if code: + data = { + "client_id": ORCID_CLIENT_ID + , "client_secret": ORCID_CLIENT_SECRET + , "grant_type": "authorization_code" + , "code": code + } + result = requests.post(ORCID_TOKEN_URL, data=data) + result_dict = json.loads(result.text.encode("utf-8")) + + user_details = get_user_by_unique_column("orcid", result_dict["orcid"]) + if user_details == None: + user_details = { + "user_id": str(uuid4()) + , "name": result_dict["name"] + , "orcid": result_dict["orcid"] + , "user_url": "%s/%s" % ( + "/".join(ORCID_AUTH_URL.split("/")[:-2]), + result_dict["orcid"]) + , "login_type": "orcid" + , "organization": "" + , "active": 1 + , "confirmed": 1 + } + save_user(user_details, user_details["user_id"]) + + url = "/n/login?type=orcid&uid="+user_details["user_id"] + else: + flash("There was an error getting code from ORCID") + return redirect(url) + +def get_github_user_details(access_token): + from utility.tools import GITHUB_API_URL + result = requests.get(GITHUB_API_URL, params={"access_token":access_token}) + return result.json() + +class LoginUser(object): + remember_time = 60 * 60 * 24 * 30 # One month in seconds + + def __init__(self): + self.remember_me = False + self.logged_in = False + + def oauth2_login(self, login_type, user_id): + """Login via an OAuth2 provider""" + + user_details = get_user_by_unique_column("user_id", user_id) + if user_details: + user = model.User() + user.id = user_details["user_id"] if user_details["user_id"] == None else "N/A" + user.full_name = user_details["name"] + user.login_type = user_details["login_type"] + return self.actual_login(user) + else: + flash("Error logging in via OAuth2") + return make_response(redirect(url_for('login'))) + + def standard_login(self): + """Login through the normal form""" + params = request.form if request.form else request.args + logger.debug("in login params are:", params) + + if not params: + from utility.tools import GITHUB_AUTH_URL, GITHUB_CLIENT_ID, ORCID_AUTH_URL, ORCID_CLIENT_ID + external_login = {} + if GITHUB_AUTH_URL and GITHUB_CLIENT_ID != 'UNKNOWN': + external_login["github"] = GITHUB_AUTH_URL + if ORCID_AUTH_URL and ORCID_CLIENT_ID != 'UNKNOWN': + external_login["orcid"] = ORCID_AUTH_URL + + return render_template( + "new_security/login_user.html" + , external_login=external_login + , redis_is_available = is_redis_available()) + else: + user_details = get_user_by_unique_column("email_address", params["email_address"]) + #user_details = get_user_by_unique_column(es, "email_address", params["email_address"]) + user = None + valid = None + if user_details: + user = model.User(); + for key in user_details: + user.__dict__[key] = user_details[key] + valid = False; + + submitted_password = params['password'] + pwfields = Struct(json.loads(user.password)) + encrypted = Password( + submitted_password, + pwfields.salt, + pwfields.iterations, + pwfields.keylength, + pwfields.hashfunc) + logger.debug("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password)) + valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password) + logger.debug("valid is:", valid) + + if valid and not user.confirmed: + VerificationEmail(user) + return render_template("new_security/verification_still_needed.html", + subject=VerificationEmail.subject) + if valid: + if params.get('remember'): + logger.debug("I will remember you") + self.remember_me = True + + if 'import_collections' in params: + import_col = "true" + else: + import_col = "false" + + #g.cookie_session.import_traits_to_user() + + self.logged_in = True + + return self.actual_login(user, import_collections=import_col) + + else: + if user: + self.unsuccessful_login(user) + flash("Invalid email-address or password. Please try again.", "alert-danger") + response = make_response(redirect(url_for('login'))) + + return response + + def actual_login(self, user, assumed_by=None, import_collections=None): + """The meat of the logging in process""" + session_id_signed = self.successful_login(user, assumed_by) + flash("Thank you for logging in {}.".format(user.full_name), "alert-success") + response = make_response(redirect(url_for('index_page', import_collections=import_collections))) + if self.remember_me: + max_age = self.remember_time + else: + max_age = None + + response.set_cookie(UserSession.cookie_name, session_id_signed, max_age=max_age) + return response + + def successful_login(self, user, assumed_by=None): + login_rec = model.Login(user) + login_rec.successful = True + login_rec.session_id = str(uuid.uuid4()) + login_rec.assumed_by = assumed_by + #session_id = "session_id:{}".format(login_rec.session_id) + session_id_signature = actual_hmac_creation(login_rec.session_id) + session_id_signed = login_rec.session_id + ":" + session_id_signature + logger.debug("session_id_signed:", session_id_signed) + + if not user.id: + user.id = '' + + session = dict(login_time = time.time(), + user_id = user.id, + user_name = user.full_name, + user_email_address = user.email_address) + + key = UserSession.cookie_name + ":" + login_rec.session_id + logger.debug("Key when signing:", key) + Redis.hmset(key, session) + if self.remember_me: + expire_time = self.remember_time + else: + expire_time = THREE_DAYS + Redis.expire(key, expire_time) + + return session_id_signed + + def unsuccessful_login(self, user): + login_rec = model.Login(user) + login_rec.successful = False + db_session.add(login_rec) + db_session.commit() + +@app.route("/n/logout") +def logout(): + logger.debug("Logging out...") + UserSession().delete_session() + flash("You are now logged out. We hope you come back soon!") + response = make_response(redirect(url_for('index_page'))) + # Delete the cookie + response.set_cookie(UserSession.cookie_name, '', expires=0) + return response + + +@app.route("/n/forgot_password", methods=['GET']) +def forgot_password(): + """Entry point for forgotten password""" + print("ARGS: ", request.args) + errors = {"no-email": request.args.get("no-email")} + print("ERRORS: ", errors) + return render_template("new_security/forgot_password.html", errors=errors) + +@app.route("/n/forgot_password_submit", methods=('POST',)) +def forgot_password_submit(): + """When a forgotten password form is submitted we get here""" + params = request.form + email_address = params['email_address'] + next_page = None + if email_address != "": + logger.debug("Wants to send password E-mail to ",email_address) + user_details = get_user_by_unique_column("email_address", email_address) + if user_details: + ForgotPasswordEmail(user_details["email_address"]) + return render_template("new_security/forgot_password_step2.html", + subject=ForgotPasswordEmail.subject) + else: + flash("The e-mail entered is not associated with an account.", "alert-danger") + return redirect(url_for("forgot_password")) + + else: + flash("You MUST provide an email", "alert-danger") + return redirect(url_for("forgot_password")) + +@app.errorhandler(401) +def unauthorized(error): + return redirect(url_for('login')) + +def is_redis_available(): + try: + Redis.ping() + except: + return False + return True + +### +# ZS: The following 6 functions require the old MySQL User accounts; I'm leaving them commented out just in case we decide to reimplement them using ElasticSearch +### +#def super_only(): +# try: +# superuser = g.user_session.user_ob.superuser +# except AttributeError: +# superuser = False +# if not superuser: +# flash("You must be a superuser to access that page.", "alert-error") +# abort(401) + +#@app.route("/manage/users") +#def manage_users(): +# super_only() +# template_vars = UsersManager() +# return render_template("admin/user_manager.html", **template_vars.__dict__) + +#@app.route("/manage/user") +#def manage_user(): +# super_only() +# template_vars = UserManager(request.args) +# return render_template("admin/ind_user_manager.html", **template_vars.__dict__) + +#@app.route("/manage/groups") +#def manage_groups(): +# super_only() +# template_vars = GroupsManager(request.args) +# return render_template("admin/group_manager.html", **template_vars.__dict__) + +#@app.route("/manage/make_superuser") +#def make_superuser(): +# super_only() +# params = request.args +# user_id = params['user_id'] +# user = model.User.query.get(user_id) +# superuser_info = basic_info() +# superuser_info['crowned_by'] = g.user_session.user_id +# user.superuser = json.dumps(superuser_info, sort_keys=True) +# db_session.commit() +# flash("We've made {} a superuser!".format(user.name_and_org)) +# return redirect(url_for("manage_users")) + +#@app.route("/manage/assume_identity") +#def assume_identity(): +# super_only() +# params = request.args +# user_id = params['user_id'] +# user = model.User.query.get(user_id) +# assumed_by = g.user_session.user_id +# return LoginUser().actual_login(user, assumed_by=assumed_by) + + +@app.route("/n/register", methods=('GET', 'POST')) +def register(): + params = None + errors = None + + + params = request.form if request.form else request.args + params = params.to_dict(flat=True) + + if params: + logger.debug("Attempting to register the user...") + result = RegisterUser(params) + errors = result.errors + + if len(errors) == 0: + flash("Registration successful. You may login with your new account", "alert-info") + return redirect(url_for("login")) + + return render_template("new_security/register_user.html", values=params, errors=errors) + + +################################# 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 = "&" + else: + combiner = "?" + return url + combiner + "hm=" + hm + +def data_hmac(stringy): + """Takes arbitray data string and appends :hmac so we know data hasn't been tampered with""" + return stringy + ":" + actual_hmac_creation(stringy) + + +def verify_url_hmac(url): + """Pass in a url that was created with url_hmac and this assures it hasn't been tampered with""" + logger.debug("url passed in to verify is:", url) + # Verify parts are correct at the end - we expect to see &hm= or ?hm= followed by an hmac + assert url[-23:-20] == "hm=", "Unexpected url (stage 1)" + assert url[-24] in ["?", "&"], "Unexpected url (stage 2)" + hmac = url[-20:] + url = url[:-24] # Url without any of the hmac stuff + + #logger.debug("before urlsplit, url is:", url) + #url = divide_up_url(url)[1] + #logger.debug("after urlsplit, url is:", 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) + hm = hmaced.hexdigest() + # "Conventional wisdom is that you don't lose much in terms of security if you throw away up to half of the output." + # http://www.w3.org/QA/2009/07/hmac_truncation_in_xml_signatu.html + hm = hm[:20] + return hm + +app.jinja_env.globals.update(url_for_hmac=url_for_hmac, + data_hmac=data_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 send_email(toaddr, msg, fromaddr="no-reply@genenetwork.org"): + """Send an E-mail through SMTP_CONNECT host. If SMTP_USERNAME is not + 'UNKNOWN' TLS is used + + """ + if SMTP_USERNAME == 'UNKNOWN': + logger.debug("SMTP: connecting with host "+SMTP_CONNECT) + server = SMTP(SMTP_CONNECT) + server.sendmail(fromaddr, toaddr, msg) + else: + logger.debug("SMTP: connecting TLS with host "+SMTP_CONNECT) + server = SMTP(SMTP_CONNECT) + server.starttls() + logger.debug("SMTP: login with user "+SMTP_USERNAME) + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logger.debug("SMTP: "+fromaddr) + logger.debug("SMTP: "+toaddr) + logger.debug("SMTP: "+msg) + server.sendmail(fromaddr, toaddr, msg) + server.quit() + logger.info("Successfully sent email to "+toaddr) + +class GroupsManager(object): + def __init__(self, kw): + self.datasets = create_datasets_list() + + +class RolesManager(object): + def __init__(self): + self.roles = model.Role.query.all() + logger.debug("Roles are:", self.roles) -- cgit v1.2.3