aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPjotr Prins2018-03-19 10:52:28 +0100
committerGitHub2018-03-19 10:52:28 +0100
commitb359cb0712e9ef923d35524f310d841581d48f30 (patch)
tree3c4cafc0bc0b481490674c807f279b4371130d0c
parent2602be69f2869de376d1b9ced6131d880e9476c2 (diff)
parent5ccf077f53c6546bb9258c7116b4b1cf8903375f (diff)
downloadgenenetwork2-b359cb0712e9ef923d35524f310d841581d48f30.tar.gz
Merge pull request #5 from fredmanglis/testing
Testing
-rwxr-xr-xbin/genenetwork225
-rwxr-xr-xbin/mechnical-rob111
-rwxr-xr-xbin/test-website114
-rw-r--r--etc/default_settings.py18
-rw-r--r--test/requests/link_checker.py63
-rw-r--r--test/requests/main_web_functionality.py40
-rw-r--r--test/requests/mapping_tests.py43
-rw-r--r--test/requests/parametrized_test.py27
-rw-r--r--test/requests/run-integration-tests.py34
-rwxr-xr-xtest/requests/test-website.py71
-rw-r--r--test/requests/test_login_github.py47
-rw-r--r--test/requests/test_login_local.py60
-rw-r--r--test/requests/test_login_orcid.py47
-rw-r--r--test/requests/test_registration.py41
-rw-r--r--test/unittest/test_registration.py27
-rw-r--r--wqflask/mock/__init__.py0
-rw-r--r--wqflask/mock/es_double.py15
-rw-r--r--wqflask/run_gunicorn.py3
-rw-r--r--wqflask/utility/elasticsearch_tools.py54
-rw-r--r--wqflask/utility/tools.py20
-rw-r--r--wqflask/wqflask/marker_regression/marker_regression.py2
-rw-r--r--wqflask/wqflask/templates/new_security/login_user.html41
-rw-r--r--wqflask/wqflask/user_manager.py318
23 files changed, 1013 insertions, 208 deletions
diff --git a/bin/genenetwork2 b/bin/genenetwork2
index 8886e4bc..2a83a1cd 100755
--- a/bin/genenetwork2
+++ b/bin/genenetwork2
@@ -95,6 +95,13 @@ export WQFLASK_OVERRIDES=$overrides # JSON
echo WQFLASK_SETTINGS=$settings
echo WQFLASK_OVERRIDES=$overrides
+# This is a temporary hack to inject ES - should have added python2-elasticsearch package to guix instead
+# if [ -z $ELASTICSEARCH_PROFILE ]; then
+# echo -e "WARNING: Elastic Search profile has not been set - use ELASTICSEARCH_PROFILE";
+# else
+# PYTHONPATH="$PYTHONPATH${PYTHONPATH:+:}$ELASTICSEARCH_PROFILE/lib/python2.7/site-packages"
+# fi
+
if [ -z $GN2_PROFILE ] ; then
echo "WARNING: GN2_PROFILE has not been set - you need the environment, so I hope you know what you are doing!"
export GN2_PROFILE=$(dirname $(dirname $(which genenetwork2)))
@@ -108,7 +115,7 @@ if [ -z $GN2_PROFILE ]; then
read -p "PRESS [ENTER] TO CONTINUE..."
else
export PATH=$GN2_PROFILE/bin:$PATH
- export PYTHONPATH=$GN2_PROFILE/lib/python2.7/site-packages
+ export PYTHONPATH="$GN2_PROFILE/lib/python2.7/site-packages${PYTHONPATH:+:}$PYTHONPATH"
export R_LIBS_SITE=$GN2_PROFILE/site-library
export GEM_PATH=$GN2_PROFILE/lib/ruby/gems/2.4.0
export JS_GUIX_PATH=$GN2_PROFILE/share/genenetwork2/javascript
@@ -124,7 +131,11 @@ else
if [ -z $GEMMA_WRAPPER_COMMAND ]; then
export GEMMA_WRAPPER_COMMAND="$GN2_PROFILE/bin/gemma-wrapper"
fi
- if [ ! -d $PYTHONPATH ] ; then echo "PYTHONPATH not valid "$PYTHONPATH ; exit 1 ; fi
+ while IFS=":" read -ra PPATH; do
+ for PPart in "${PPATH[@]}"; do
+ if [ ! -d $PPart ] ; then echo "PYTHONPATH not valid "$PYTHONPATH ; exit 1 ; fi
+ done
+ done <<< "$PYTHONPATH"
if [ ! -d $R_LIBS_SITE ] ; then echo "R_LIBS_SITE not valid "$R_LIBS_SITE ; exit 1 ; fi
if [ ! -d $GEM_PATH ] ; then echo "GEM_PATH not valid "$GEM_PATH ; exit 1 ; fi
fi
@@ -157,8 +168,9 @@ if [ "$1" = '-c' ] ; then
cd $GN2_BASE_DIR/wqflask
cmd=${2#wqflask/}
echo PYTHONPATH=$PYTHONPATH
- echo RUNNING COMMAND $cmd
- python $cmd
+ shift ; shift
+ echo RUNNING COMMAND $cmd $*
+ python $cmd $*
exit $?
fi
# Now handle command parameter -cli which runs in bash
@@ -167,8 +179,9 @@ if [ "$1" = "-cli" ] ; then
cd $GN2_BASE_DIR/wqflask
cmd=$2
echo PYTHONPATH=$PYTHONPATH
- echo RUNNING COMMAND $cmd
- $cmd
+ shift ; shift
+ echo RUNNING COMMAND $cmd $*
+ $cmd $*
exit $?
fi
if [ "$1" = '-gunicorn' ] ; then
diff --git a/bin/mechnical-rob b/bin/mechnical-rob
new file mode 100755
index 00000000..be223d94
--- /dev/null
+++ b/bin/mechnical-rob
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+
+
+USAGE = <<EOT
+This is Mechanical-Rob - an automated web server tester for
+ Genenetwork.org that uses the brilliant
+ mechanize gem with minitest.
+
+To use this software you need to install mechanize. Run it from
+the root of the genenetwork2 source tree as, for example,
+
+ ./bin/test-website http://localhost:5003/ (default)
+
+If you are using the small deployment database you can use
+
+ ./bin/test-website --skip -n
+
+To run all tests
+
+ ./bin/test-website --all
+
+To run individual tests on localhost you can do
+
+ ruby -Itest -Itest/lib test/lib/mapping.rb --name="/Mapping/"
+
+For more information see http://genenetwork.org/
+
+EOT
+$stderr.print USAGE
+
+require 'optparse'
+
+options = { database: :small, link_checker: false}
+opts = OptionParser.new do |o|
+ o.banner = "Usage: #{File.basename($0)} [options] URL"
+
+ o.on('-d','--database', String, 'Use database (default db_webqtl_s)') do |s|
+ options[:database] =
+ case s
+ when 'xx'
+ :unknown
+ else
+ :small
+ end
+ end
+
+ o.on('--all', 'Run all tests') do
+ options[:all] = true
+ end
+
+ o.on('-l','--link-checker', 'Check for dead links') do
+ options[:link_checker] = true
+ end
+
+ o.on('--navigation', 'Check for navigation') do
+ options[:navigation] = true
+ end
+
+ o.on('--mapping', 'Check for mapping') do
+ options[:mapping] = true
+ end
+
+ o.on('--skip-broken', 'Skip tests that are known to be broken') do
+ options[:skip_broken] = true
+ end
+
+ o.separator ""
+ o.on_tail('-h', '--help', 'display this help and exit') do
+ options[:show_help] = true
+ end
+end
+
+opts.parse!(ARGV)
+
+if options[:show_help]
+ print opts
+ exit 1
+end
+
+$options = options # we are using a global here
+$host =
+ if ARGV.size>0
+ ARGV.shift
+ else
+ "http://localhost:5003"
+ end
+
+$stderr.print "Testing <",$host,">\n"
+
+require 'mechanize'
+require 'minitest/spec'
+require 'minitest/autorun'
+
+# These are the actual testing modules
+
+libpath = File.dirname(File.dirname(__FILE__))
+$: << File.join(libpath,'test/lib')
+
+require 'main_web_functionality'
+
+if options[:all] or options[:mapping]
+ require 'mapping'
+end
+
+if options[:all] or options[:link_checker]
+ require 'link_checker'
+end
+
+if options[:all] or options[:navigation]
+ require 'navigation'
+end
diff --git a/bin/test-website b/bin/test-website
index be223d94..5935f016 100755
--- a/bin/test-website
+++ b/bin/test-website
@@ -1,111 +1,7 @@
-#!/usr/bin/env ruby
+#! /bin/bash
-
-USAGE = <<EOT
-This is Mechanical-Rob - an automated web server tester for
- Genenetwork.org that uses the brilliant
- mechanize gem with minitest.
-
-To use this software you need to install mechanize. Run it from
-the root of the genenetwork2 source tree as, for example,
-
- ./bin/test-website http://localhost:5003/ (default)
-
-If you are using the small deployment database you can use
-
- ./bin/test-website --skip -n
-
-To run all tests
-
- ./bin/test-website --all
-
-To run individual tests on localhost you can do
-
- ruby -Itest -Itest/lib test/lib/mapping.rb --name="/Mapping/"
-
-For more information see http://genenetwork.org/
-
-EOT
-$stderr.print USAGE
-
-require 'optparse'
-
-options = { database: :small, link_checker: false}
-opts = OptionParser.new do |o|
- o.banner = "Usage: #{File.basename($0)} [options] URL"
-
- o.on('-d','--database', String, 'Use database (default db_webqtl_s)') do |s|
- options[:database] =
- case s
- when 'xx'
- :unknown
- else
- :small
- end
- end
-
- o.on('--all', 'Run all tests') do
- options[:all] = true
- end
-
- o.on('-l','--link-checker', 'Check for dead links') do
- options[:link_checker] = true
- end
-
- o.on('--navigation', 'Check for navigation') do
- options[:navigation] = true
- end
-
- o.on('--mapping', 'Check for mapping') do
- options[:mapping] = true
- end
-
- o.on('--skip-broken', 'Skip tests that are known to be broken') do
- options[:skip_broken] = true
- end
-
- o.separator ""
- o.on_tail('-h', '--help', 'display this help and exit') do
- options[:show_help] = true
- end
-end
-
-opts.parse!(ARGV)
-
-if options[:show_help]
- print opts
+if [ -z $GN2_PROFILE ]; then
+ echo "Run request tests with something like"
+ echo env GN2_PROFILE=/home/wrk/opt/gn-latest ./bin/genenetwork2 ./etc/default_settings.py -c ../test/requests/test-website.py http://localhost:5003
exit 1
-end
-
-$options = options # we are using a global here
-$host =
- if ARGV.size>0
- ARGV.shift
- else
- "http://localhost:5003"
- end
-
-$stderr.print "Testing <",$host,">\n"
-
-require 'mechanize'
-require 'minitest/spec'
-require 'minitest/autorun'
-
-# These are the actual testing modules
-
-libpath = File.dirname(File.dirname(__FILE__))
-$: << File.join(libpath,'test/lib')
-
-require 'main_web_functionality'
-
-if options[:all] or options[:mapping]
- require 'mapping'
-end
-
-if options[:all] or options[:link_checker]
- require 'link_checker'
-end
-
-if options[:all] or options[:navigation]
- require 'navigation'
-end
+fi
diff --git a/etc/default_settings.py b/etc/default_settings.py
index 699d21f1..a70d8aec 100644
--- a/etc/default_settings.py
+++ b/etc/default_settings.py
@@ -41,6 +41,24 @@ SECURITY_POST_LOGIN_VIEW = "/thank_you"
SERVER_PORT = 5003 # running on localhost
SECRET_HMAC_CODE = '\x08\xdf\xfa\x93N\x80\xd9\\H@\\\x9f`\x98d^\xb4a;\xc6OM\x946a\xbc\xfc\x80:*\xebc'
+GITHUB_CLIENT_ID = "UNKNOWN"
+GITHUB_CLIENT_SECRET = "UNKNOWN"
+GITHUB_AUTH_URL = "UNKNOWN"
+GITHUB_API_URL = "UNKNOWN"
+
+ORCID_CLIENT_ID = "UNKNOWN"
+ORCID_CLIENT_SECRET = "UNKNOWN"
+ORCID_AUTH_URL = "UNKNOWN"
+ORCID_TOKEN_URL = "UNKNOWN"
+
+ELASTICSEARCH_HOST = "localhost"
+ELASTICSEARCH_PORT = '9200'
+
+SMTP_CONNECT = "UNKNOWN"
+SMTP_USERNAME = "UNKNOWN"
+SMTP_PASSWORD = "UNKNOWN"
+
+
# ---- Behavioural settings (defaults) note that logger and log levels can
# be overridden at the module level and with enviroment settings
WEBSERVER_MODE = 'DEV' # Python webserver mode (DEBUG|DEV|PROD)
diff --git a/test/requests/link_checker.py b/test/requests/link_checker.py
new file mode 100644
index 00000000..256bf6ef
--- /dev/null
+++ b/test/requests/link_checker.py
@@ -0,0 +1,63 @@
+from __future__ import print_function
+import re
+import requests
+from lxml.html import parse
+from requests.exceptions import ConnectionError
+
+def is_root_link(link):
+ pattern = re.compile("^/$")
+ return pattern.match(link)
+
+def is_mailto_link(link):
+ pattern = re.compile("^mailto:.*")
+ return pattern.match(link)
+
+def is_internal_link(link):
+ pattern = re.compile("^/.*")
+ return pattern.match(link)
+
+def get_links(doc):
+ return filter(
+ lambda x: not (
+ is_root_link(x)
+ or is_mailto_link(x))
+ , map(lambda y: y.get("href")
+ , doc.cssselect("a")))
+
+def verify_link(link):
+ try:
+ result = requests.get(link, timeout=20)
+ if result.status_code == 200:
+ print(link+" ==> OK")
+ else:
+ print("ERROR: link `"+link+"` failed with status "
+ , result.status_code)
+ except ConnectionError as ex:
+ print("ERROR: ", link, ex)
+
+def check_page(host, start_url):
+ print("")
+ print("Checking links in page `"+start_url+"`")
+ doc = parse(start_url).getroot()
+ links = get_links(doc)
+ internal_links = filter(is_internal_link, links)
+ external_links = filter(lambda x: not is_internal_link(x), links)
+ external_links.append("http://somenon-existentsite.brr")
+ for link in internal_links:
+ verify_link(host+link)
+
+ for link in external_links:
+ verify_link(link)
+
+def check_links(args_obj, parser):
+ print("")
+ print("Checking links")
+ host = args_obj.host
+
+ # Check the home page
+ check_page(host, host)
+
+ # Check traits page
+ check_page(
+ host,
+ host+"/show_trait?trait_id=1435395_s_at&dataset=HC_M2_0606_P")
diff --git a/test/requests/main_web_functionality.py b/test/requests/main_web_functionality.py
new file mode 100644
index 00000000..7b89b833
--- /dev/null
+++ b/test/requests/main_web_functionality.py
@@ -0,0 +1,40 @@
+from __future__ import print_function
+import re
+import requests
+from lxml.html import parse
+from link_checker import check_page
+from requests.exceptions import ConnectionError
+
+def check_home(url):
+ doc = parse(url).getroot()
+ search_button = doc.cssselect("#btsearch")
+ assert(search_button[0].value == "Search")
+ print("OK")
+
+def check_search_page(host):
+ data = dict(
+ species="mouse"
+ , group="BXD"
+ , type="Hippocampus mRNA"
+ , dataset="HC_M2_0606_P"
+ , search_terms_or=""
+ , search_terms_and="MEAN=(15 16) LRS=(23 46)")
+ result = requests.get(host+"/search", params=data)
+ found = result.text.find("/show_trait?trait_id=1435395_s_at&dataset=HC_M2_0606_P")
+ assert(found >= 0)
+ print("OK")
+ check_traits_page(host, "/show_trait?trait_id=1435395_s_at&dataset=HC_M2_0606_P")
+
+def check_traits_page(host, traits_url):
+ doc = parse(host+traits_url).getroot()
+ traits_form = doc.forms[1]
+ assert(traits_form.fields["corr_dataset"] == "HC_M2_0606_P")
+ print("OK")
+ check_page(host, host+traits_url)
+
+def check_main_web_functionality(args_obj, parser):
+ print("")
+ print("Checking main web functionality...")
+ host = args_obj.host
+ check_home(host)
+ check_search_page(host)
diff --git a/test/requests/mapping_tests.py b/test/requests/mapping_tests.py
new file mode 100644
index 00000000..fd20df11
--- /dev/null
+++ b/test/requests/mapping_tests.py
@@ -0,0 +1,43 @@
+from __future__ import print_function
+import re
+import json
+import requests
+from lxml.html import fromstring
+
+def get_data(list_item):
+ try:
+ value = list_item[1]
+ except:
+ value = None
+ #print("list_item:", list_item, "==>", value)
+ return value
+
+def load_data_from_file():
+ filename = "../test/data/input/mapping/1435395_s_at_HC_M2_0606_P.json"
+ file_handle = open(filename, "r")
+ file_data = json.loads(file_handle.read().encode("utf-8"))
+ return file_data
+
+def check_pylmm_tool_selection(host, data):
+ data["method"] = "pylmm"
+ page = requests.post(host+"/marker_regression", data=data)
+ doc = fromstring(page.text)
+ form = doc.forms[1]
+ assert form.fields["dataset"] == "HC_M2_0606_P"
+ assert form.fields["value:BXD1"] == "15.034" # Check value in the file
+
+def check_R_qtl_tool_selection(host, data):
+ pass
+
+def check_CIM_tool_selection(host, data):
+ pass
+
+def check_mapping(args_obj, parser):
+ print("")
+ print("Checking mapping")
+
+ host = args_obj.host
+ data = load_data_from_file()
+ check_pylmm_tool_selection(host, data)
+ check_R_qtl_tool_selection(host, data)
+ check_CIM_tool_selection(host, data)
diff --git a/test/requests/parametrized_test.py b/test/requests/parametrized_test.py
new file mode 100644
index 00000000..abf98fce
--- /dev/null
+++ b/test/requests/parametrized_test.py
@@ -0,0 +1,27 @@
+import logging
+import unittest
+from elasticsearch import Elasticsearch, TransportError
+
+class ParametrizedTest(unittest.TestCase):
+
+ def __init__(self, methodName='runTest', gn2_url="http://localhost:5003", es_url="localhost:9200"):
+ super(ParametrizedTest, self).__init__(methodName=methodName)
+ self.gn2_url = gn2_url
+ self.es_url = es_url
+
+ def setUp(self):
+ self.es = Elasticsearch([self.es_url])
+ self.es_cleanup = []
+
+ es_logger = logging.getLogger("elasticsearch")
+ es_logger.addHandler(
+ logging.FileHandler("/tmp/es_TestRegistrationInfo.log"))
+ es_trace_logger = logging.getLogger("elasticsearch.trace")
+ es_trace_logger.addHandler(
+ logging.FileHandler("/tmp/es_TestRegistrationTrace.log"))
+
+ def tearDown(self):
+ self.es.delete_by_query(
+ index="users"
+ , doc_type="local"
+ , body={"query":{"match":{"email_address":"test@user.com"}}})
diff --git a/test/requests/run-integration-tests.py b/test/requests/run-integration-tests.py
new file mode 100644
index 00000000..5e816549
--- /dev/null
+++ b/test/requests/run-integration-tests.py
@@ -0,0 +1,34 @@
+import sys
+from test_login_local import TestLoginLocal
+from test_login_orcid import TestLoginOrcid
+from test_login_github import TestLoginGithub
+from test_registration import TestRegistration
+from unittest import TestSuite, TextTestRunner, TestLoader
+
+test_cases = [
+ TestRegistration
+ , TestLoginLocal
+ , TestLoginGithub
+ , TestLoginOrcid
+]
+
+def suite(gn2_url, es_url):
+ the_suite = TestSuite()
+ for case in test_cases:
+ the_suite.addTests(initTest(case, gn2_url, es_url))
+ return the_suite
+
+def initTest(klass, gn2_url, es_url):
+ loader = TestLoader()
+ methodNames = loader.getTestCaseNames(klass)
+ return [klass(mname, gn2_url, es_url) for mname in methodNames]
+
+def main(gn2_url, es_url):
+ runner = TextTestRunner()
+ runner.run(suite(gn2_url, es_url))
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ raise Exception("Required arguments missing:\n\tTry running `run-integration-test.py <gn2-url> <es-url>`")
+ else:
+ main(sys.argv[1], sys.argv[2])
diff --git a/test/requests/test-website.py b/test/requests/test-website.py
new file mode 100755
index 00000000..2bef6eb1
--- /dev/null
+++ b/test/requests/test-website.py
@@ -0,0 +1,71 @@
+# Run with something like
+#
+# env GN2_PROFILE=/home/wrk/opt/gn-latest ./bin/genenetwork2 ./etc/default_settings.py -c ../test/requests/test-website.py http://localhost:5003
+#
+# Mostly to pick up the Guix GN2_PROFILE and python modules
+from __future__ import print_function
+import argparse
+from link_checker import check_links
+from mapping_tests import check_mapping
+from main_web_functionality import check_main_web_functionality
+
+print("Mechanical Rob firing up...")
+
+def run_all(args_obj, parser):
+ print("")
+ print("Running all tests.")
+ check_main_web_functionality(args_obj, parser)
+ check_links(args_obj, parser)
+ check_mapping(args_obj, parser)
+ # TODO: Add other functions as they are created.
+
+def print_help(args_obj, parser):
+ print(parser.format_help())
+
+def dummy(args_obj, parser):
+ print("Not implemented yet.")
+
+
+desc = """
+This is Mechanical-Rob - an automated web server tester for
+ Genenetwork.org
+"""
+parser = argparse.ArgumentParser(description=desc)
+
+parser.add_argument("-d", "--database", metavar="DB", type=str
+ , default="db_webqtl_s"
+ , help="Use database (default db_webqtl_s)")
+
+parser.add_argument("host", metavar="HOST", type=str
+ , default="http://localhost:5003"
+ , help="The url to the web server")
+
+parser.add_argument("-a", "--all", dest="accumulate", action="store_const"
+ , const=run_all, default=print_help
+ , help="Runs all tests.")
+
+parser.add_argument("-l", "--link-checker", dest="accumulate"
+ , action='store_const', const=check_links, default=print_help
+ , help="Checks for dead links.")
+
+parser.add_argument("-f", "--main-functionality", dest="accumulate"
+ , action='store_const', const=check_main_web_functionality
+ , default=print_help
+ , help="Checks for main web functionality.")
+
+parser.add_argument("-m", "--mapping", dest="accumulate"
+ , action="store_const", const=check_mapping, default=print_help
+ , help="Checks for mapping.")
+
+# parser.add_argument("-n", "--navigation", dest="accumulate"
+# , action="store_const", const=check_navigation, default=print_help
+# , help="Checks for navigation.")
+
+# parser.add_argument("-s", "--skip-broken", dest="accumulate"
+# , action="store_const", const=dummy, default=print_help
+# , help="Skip tests that are known to be broken.")
+
+args = parser.parse_args()
+# print("The arguments object: ", args)
+
+args.accumulate(args, parser)
diff --git a/test/requests/test_login_github.py b/test/requests/test_login_github.py
new file mode 100644
index 00000000..1bf4f695
--- /dev/null
+++ b/test/requests/test_login_github.py
@@ -0,0 +1,47 @@
+import uuid
+import requests
+from time import sleep
+from wqflask import app
+from parameterized import parameterized
+from parametrized_test import ParametrizedTest
+
+login_link_text = '<a id="login_in" href="/n/login">Sign in</a>'
+logout_link_text = '<a id="login_out" title="Signed in as ." href="/n/logout">Sign out</a>'
+uid = str(uuid.uuid4())
+
+class TestLoginGithub(ParametrizedTest):
+
+ def setUp(self):
+ super(TestLoginGithub, self).setUp()
+ data = {
+ "user_id": uid
+ , "name": "A. T. Est User"
+ , "github_id": 693024
+ , "user_url": "https://fake-github.com/atestuser"
+ , "login_type": "github"
+ , "organization": ""
+ , "active": 1
+ , "confirmed": 1
+ }
+ self.es.create(index="users", doc_type="local", body=data, id=uid)
+ sleep(1)
+
+ def tearDown(self):
+ super(TestLoginGithub, self).tearDown()
+ self.es.delete(index="users", doc_type="local", id=uid)
+
+ def testLoginUrl(self):
+ login_button_text = '<a href="https://github.com/login/oauth/authorize?client_id=' + app.config.get("GITHUB_CLIENT_ID") + '&amp;client_secret=' + app.config.get("GITHUB_CLIENT_SECRET") + '" title="Login with GitHub" class="btn btn-info btn-group">Login with Github</a>'
+ result = requests.get(self.gn2_url+"/n/login")
+ index = result.content.find(login_button_text)
+ self.assertTrue(index >= 0, "Should have found `Login with Github` button")
+
+ @parameterized.expand([
+ ("1234", login_link_text, "Login should have failed with non-existing user")
+ , (uid, logout_link_text, "Login should have been successful with existing user")
+ ])
+ def testLogin(self, test_uid, expected, message):
+ url = self.gn2_url+"/n/login?type=github&uid="+test_uid
+ result = requests.get(url)
+ index = result.content.find(expected)
+ self.assertTrue(index >= 0, message)
diff --git a/test/requests/test_login_local.py b/test/requests/test_login_local.py
new file mode 100644
index 00000000..808649ca
--- /dev/null
+++ b/test/requests/test_login_local.py
@@ -0,0 +1,60 @@
+import requests
+from wqflask import user_manager
+from parameterized import parameterized
+from parametrized_test import ParametrizedTest
+
+login_link_text = '<a id="login_in" href="/n/login">Sign in</a>'
+logout_link_text = '<a id="login_out" title="Signed in as ." href="/n/logout">Sign out</a>'
+
+class TestLoginLocal(ParametrizedTest):
+
+ def setUp(self):
+ super(TestLoginLocal, self).setUp()
+ self.login_url = self.gn2_url +"/n/login"
+ data = {
+ "es_connection": self.es,
+ "email_address": "test@user.com",
+ "full_name": "Test User",
+ "organization": "Test Organisation",
+ "password": "test_password",
+ "password_confirm": "test_password"
+ }
+ user_manager.basic_info = lambda : { "basic_info": "basic" }
+ user_manager.RegisterUser(data)
+
+
+ @parameterized.expand([
+ (
+ {
+ "email_address": "non@existent.email",
+ "password": "doesitmatter?"
+ }, login_link_text, "Login should have failed with the wrong user details."),
+ (
+ {
+ "email_address": "test@user.com",
+ "password": "test_password"
+ }, logout_link_text, "Login should have been successful with correct user details and neither import_collections nor remember_me set"),
+ (
+ {
+ "email_address": "test@user.com",
+ "password": "test_password",
+ "import_collections": "y"
+ }, logout_link_text, "Login should have been successful with correct user details and only import_collections set"),
+ (
+ {
+ "email_address": "test@user.com",
+ "password": "test_password",
+ "remember_me": "y"
+ }, logout_link_text, "Login should have been successful with correct user details and only remember_me set"),
+ (
+ {
+ "email_address": "test@user.com",
+ "password": "test_password",
+ "remember_me": "y",
+ "import_collections": "y"
+ }, logout_link_text, "Login should have been successful with correct user details, and both remember_me, and import_collections set")
+ ])
+ def testLogin(self, data, expected, message):
+ result = requests.post(self.login_url, data=data)
+ index = result.content.find(expected)
+ self.assertTrue(index >= 0, message)
diff --git a/test/requests/test_login_orcid.py b/test/requests/test_login_orcid.py
new file mode 100644
index 00000000..ea15642e
--- /dev/null
+++ b/test/requests/test_login_orcid.py
@@ -0,0 +1,47 @@
+import uuid
+import requests
+from time import sleep
+from wqflask import app
+from parameterized import parameterized
+from parametrized_test import ParametrizedTest
+
+login_link_text = '<a id="login_in" href="/n/login">Sign in</a>'
+logout_link_text = '<a id="login_out" title="Signed in as ." href="/n/logout">Sign out</a>'
+uid = str(uuid.uuid4())
+
+class TestLoginOrcid(ParametrizedTest):
+
+ def setUp(self):
+ super(TestLoginOrcid, self).setUp()
+ data = {
+ "user_id": uid
+ , "name": "A. T. Est User"
+ , "orcid": 345872
+ , "user_url": "https://fake-orcid.org/atestuser"
+ , "login_type": "orcid"
+ , "organization": ""
+ , "active": 1
+ , "confirmed": 1
+ }
+ self.es.create(index="users", doc_type="local", body=data, id=uid)
+ sleep(1)
+
+ def tearDown(self):
+ super(TestLoginOrcid, self).tearDown()
+ self.es.delete(index="users", doc_type="local", id=uid)
+
+ def testLoginUrl(self):
+ login_button_text = 'a href="https://sandbox.orcid.org/oauth/authorize?response_type=code&amp;scope=/authenticate&amp;show_login=true&amp;client_id=' + app.config.get("ORCID_CLIENT_ID") + '&amp;client_secret=' + app.config.get("ORCID_CLIENT_SECRET") + '" title="Login with ORCID" class="btn btn-info btn-group">Login with ORCID</a>'
+ result = requests.get(self.gn2_url+"/n/login")
+ index = result.content.find(login_button_text)
+ self.assertTrue(index >= 0, "Should have found `Login with ORCID` button")
+
+ @parameterized.expand([
+ ("1234", login_link_text, "Login should have failed with non-existing user")
+ , (uid, logout_link_text, "Login should have been successful with existing user")
+ ])
+ def testLogin(self, test_uid, expected, message):
+ url = self.gn2_url+"/n/login?type=orcid&uid="+test_uid
+ result = requests.get(url)
+ index = result.content.find(expected)
+ self.assertTrue(index >= 0, message)
diff --git a/test/requests/test_registration.py b/test/requests/test_registration.py
new file mode 100644
index 00000000..0047e8a6
--- /dev/null
+++ b/test/requests/test_registration.py
@@ -0,0 +1,41 @@
+import sys
+import requests
+from parametrized_test import ParametrizedTest
+
+class TestRegistration(ParametrizedTest):
+
+ def tearDown(self):
+ for item in self.es_cleanup:
+ self.es.delete(index="users", doc_type="local", id=item["_id"])
+
+ def testRegistrationPage(self):
+ if self.es.ping():
+ data = {
+ "email_address": "test@user.com",
+ "full_name": "Test User",
+ "organization": "Test Organisation",
+ "password": "test_password",
+ "password_confirm": "test_password"
+ }
+ requests.post(self.gn2_url+"/n/register", data)
+ response = self.es.search(
+ index="users"
+ , doc_type="local"
+ , body={
+ "query": {"match": {"email_address": "test@user.com"}}})
+ self.assertEqual(len(response["hits"]["hits"]), 1)
+ else:
+ self.skipTest("The elasticsearch server is down")
+
+def main(gn2, es):
+ import unittest
+ suite = unittest.TestSuite()
+ suite.addTest(TestRegistration(methodName="testRegistrationPage", gn2_url=gn2, es_url=es))
+ runner = unittest.TextTestRunner()
+ runner.run(suite)
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ raise Exception("Required arguments missing")
+ else:
+ main(sys.argv[1], sys.argv[2])
diff --git a/test/unittest/test_registration.py b/test/unittest/test_registration.py
new file mode 100644
index 00000000..98d0cdff
--- /dev/null
+++ b/test/unittest/test_registration.py
@@ -0,0 +1,27 @@
+# Run test with something like
+#
+# env GN2_PROFILE=~/opt/gn-latest GENENETWORK_FILES=$HOME/gn2_data ./bin/genenetwork2 ./etc/default_settings.py -c ../test/unittest/test_registration.py
+#
+
+import unittest
+import mock.es_double as es
+from wqflask.user_manager import RegisterUser
+
+class TestRegisterUser(unittest.TestCase):
+ def setUp(self):
+ self.es = es.ESDouble()
+
+ def testRegisterUserWithCorrectData(self):
+ data = {
+ "email_address": "user@example.com"
+ , "full_name": "A.N. Other"
+ , "organization": "Some Organisation"
+ , "password": "testing"
+ , "password_confirm": "testing"
+ , "es_connection": self.es
+ }
+ result = RegisterUser(data)
+ self.assertEqual(len(result.errors), 0, "Errors were not expected")
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/wqflask/mock/__init__.py b/wqflask/mock/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/wqflask/mock/__init__.py
diff --git a/wqflask/mock/es_double.py b/wqflask/mock/es_double.py
new file mode 100644
index 00000000..6ef8a1b9
--- /dev/null
+++ b/wqflask/mock/es_double.py
@@ -0,0 +1,15 @@
+class ESDouble(object):
+ def __init__(self):
+ self.items = {}
+
+ def ping(self):
+ return true
+
+ def create(self, index, doc_type, body, id):
+ self.items["index"] = {doc_type: {"id": id, "_source": data}}
+
+ def search(self, index, doc_type, body):
+ return {
+ "hits": {
+ "hits": self.items[index][doc_type][body]
+ }}
diff --git a/wqflask/run_gunicorn.py b/wqflask/run_gunicorn.py
index 14a2d689..ebe3add5 100644
--- a/wqflask/run_gunicorn.py
+++ b/wqflask/run_gunicorn.py
@@ -11,6 +11,9 @@ print "Starting up Gunicorn process"
from wqflask import app
+app.config['SESSION_TYPE'] = 'filesystem'
+app.config['SECRET_KEY'] = 'super secret key'
+
@app.route("/gunicorn")
def hello():
return "<h1 style='color:blue'>Hello There!</h1>"
diff --git a/wqflask/utility/elasticsearch_tools.py b/wqflask/utility/elasticsearch_tools.py
new file mode 100644
index 00000000..2d3d5add
--- /dev/null
+++ b/wqflask/utility/elasticsearch_tools.py
@@ -0,0 +1,54 @@
+from elasticsearch import Elasticsearch, TransportError
+import logging
+
+from utility.logger import getLogger
+logger = getLogger(__name__)
+
+from utility.tools import ELASTICSEARCH_HOST, ELASTICSEARCH_PORT
+
+def get_elasticsearch_connection():
+ logger.info("get_elasticsearch_connection")
+ es = None
+ try:
+ assert(ELASTICSEARCH_HOST)
+ assert(ELASTICSEARCH_PORT)
+ logger.info("ES HOST",ELASTICSEARCH_HOST)
+
+ es = Elasticsearch([{
+ "host": ELASTICSEARCH_HOST
+ , "port": ELASTICSEARCH_PORT
+ }]) if (ELASTICSEARCH_HOST and ELASTICSEARCH_PORT) else None
+
+ es_logger = logging.getLogger("elasticsearch")
+ es_logger.setLevel(logging.INFO)
+ es_logger.addHandler(logging.NullHandler())
+ except:
+ es = None
+
+ return es
+
+def get_user_by_unique_column(es, column_name, column_value, index="users", doc_type="local"):
+ return get_item_by_unique_column(es, column_name, column_value, index=index, doc_type=doc_type)
+
+def save_user(es, user, user_id):
+ es_save_data(es, "users", "local", user, user_id)
+
+def get_item_by_unique_column(es, column_name, column_value, index, doc_type):
+ item_details = None
+ try:
+ response = es.search(
+ index = index
+ , doc_type = doc_type
+ , body = {
+ "query": { "match": { column_name: column_value } }
+ })
+ if len(response["hits"]["hits"]) > 0:
+ item_details = response["hits"]["hits"][0]["_source"]
+ except TransportError as te:
+ pass
+ return item_details
+
+def es_save_data(es, index, doc_type, data_item, data_id,):
+ from time import sleep
+ es.create(index, doc_type, body=data_item, id=data_id)
+ sleep(1) # Delay 1 second to allow indexing
diff --git a/wqflask/utility/tools.py b/wqflask/utility/tools.py
index d3113302..8c9fed96 100644
--- a/wqflask/utility/tools.py
+++ b/wqflask/utility/tools.py
@@ -251,6 +251,26 @@ assert_dir(JS_GUIX_PATH)
JS_GN_PATH = get_setting('JS_GN_PATH')
# assert_dir(JS_GN_PATH)
+GITHUB_CLIENT_ID = get_setting('GITHUB_CLIENT_ID')
+GITHUB_CLIENT_SECRET = get_setting('GITHUB_CLIENT_SECRET')
+GITHUB_AUTH_URL = None
+if GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET:
+ GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize?client_id="+GITHUB_CLIENT_ID+"&client_secret="+GITHUB_CLIENT_SECRET
+GITHUB_API_URL = get_setting('GITHUB_API_URL')
+ORCID_CLIENT_ID = get_setting('ORCID_CLIENT_ID')
+ORCID_CLIENT_SECRET = get_setting('ORCID_CLIENT_SECRET')
+ORCID_AUTH_URL = None
+if ORCID_CLIENT_ID and ORCID_CLIENT_SECRET:
+ ORCID_AUTH_URL = "https://sandbox.orcid.org/oauth/authorize?response_type=code&scope=/authenticate&show_login=true&client_id="+ORCID_CLIENT_ID+"&client_secret="+ORCID_CLIENT_SECRET
+ORCID_TOKEN_URL = get_setting('ORCID_TOKEN_URL')
+
+ELASTICSEARCH_HOST = get_setting('ELASTICSEARCH_HOST')
+ELASTICSEARCH_PORT = get_setting('ELASTICSEARCH_PORT')
+
+SMTP_CONNECT = get_setting('SMTP_CONNECT')
+SMTP_USERNAME = get_setting('SMTP_USERNAME')
+SMTP_PASSWORD = get_setting('SMTP_PASSWORD')
+
PYLMM_COMMAND = app_set("PYLMM_COMMAND",pylmm_command())
GEMMA_COMMAND = app_set("GEMMA_COMMAND",gemma_command())
assert(GEMMA_COMMAND is not None)
diff --git a/wqflask/wqflask/marker_regression/marker_regression.py b/wqflask/wqflask/marker_regression/marker_regression.py
index 087b95b4..c189fb96 100644
--- a/wqflask/wqflask/marker_regression/marker_regression.py
+++ b/wqflask/wqflask/marker_regression/marker_regression.py
@@ -114,7 +114,7 @@ class MarkerRegression(object):
self.num_perm = 0
self.perm_output = []
self.bootstrap_results = []
- self.covariates = start_vars['covariates']
+ self.covariates = start_vars['covariates'] if "covariates" in start_vars else None
#ZS: This is passed to GN1 code for single chr mapping
self.selected_chr = -1
diff --git a/wqflask/wqflask/templates/new_security/login_user.html b/wqflask/wqflask/templates/new_security/login_user.html
index b9f49a61..0dae3503 100644
--- a/wqflask/wqflask/templates/new_security/login_user.html
+++ b/wqflask/wqflask/templates/new_security/login_user.html
@@ -16,14 +16,39 @@
<h4>Don't have an account?</h4>
- <a href="/n/register" class="btn btn-primary modalize">Create a new account</a>
-
-
- <hr />
+ {% if es_server: %}
+ <a href="/n/register" class="btn btn-primary modalize">Create a new account</a>
+ {% else: %}
+ <div class="alert alert-warning">
+ <p>You cannot create an account at this moment.<br />
+ Please try again later.</p>
+ </div>
+ {% endif %}
+
+ <hr />
+ <h4>Login with external services</h4>
+
+ {% if external_login: %}
+ <div>
+ {% if external_login["github"]: %}
+ <a href="{{external_login['github']}}" title="Login with GitHub" class="btn btn-info btn-group">Login with Github</a>
+ {% endif %}
+
+ {% if external_login["orcid"]: %}
+ <a href="{{external_login['orcid']}}" title="Login with ORCID" class="btn btn-info btn-group">Login with ORCID</a>
+ {% endif %}
+ </div>
+ {% else: %}
+ <div class="alert alert-warning">
+ <p>You cannot login with external services at this time.<br />
+ Please try again later.</p>
+ </div>
+ {% endif %}
+ <hr />
<h4>Already have an account? Sign in here.</h4>
-
+ {% if es_server: %}
<form class="form-horizontal" action="/n/login" method="POST" name="login_user_form" id="loginUserForm">
<fieldset>
<div class="form-group">
@@ -61,6 +86,12 @@
</fieldset>
</form>
+ {% else: %}
+ <div class="alert alert-warning">
+ <p>You cannot login at this moment using your GeneNetwork account.<br />
+ Please try again later.</p>
+ </div>
+ {% endif %}
</div>
</div>
diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py
index f7fcd2d0..c344ea27 100644
--- a/wqflask/wqflask/user_manager.py
+++ b/wqflask/wqflask/user_manager.py
@@ -54,6 +54,9 @@ logger = getLogger(__name__)
from base.data_set import create_datasets_list
+import requests
+from utility.elasticsearch_tools import get_elasticsearch_connection, get_user_by_unique_column, save_user
+
THREE_DAYS = 60 * 60 * 24 * 3
#THREE_DAYS = 45
@@ -119,7 +122,8 @@ class AnonUser(object):
return collections
def import_traits_to_user(self):
- collections_list = json.loads(Redis.get(self.key))
+ result = Redis.get(self.key)
+ collections_list = json.loads(result if result else "[]")
for collection in collections_list:
uc = model.UserCollection()
uc.name = collection['name']
@@ -268,16 +272,24 @@ class RegisterUser(object):
self.thank_you_mode = False
self.errors = []
self.user = Bunch()
+ es = kw.get('es_connection', None)
+
+ if not es:
+ self.errors.append("Missing connection object")
- self.user.email_address = kw.get('email_address', '').strip()
+ 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(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', '').strip()
+ 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', '').strip()
+ 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.')
@@ -294,28 +306,11 @@ class RegisterUser(object):
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)
-
- self.new_user = model.User(**self.user.__dict__)
- db_session.add(self.new_user)
-
- try:
- db_session.commit()
- except sqlalchemy.exc.IntegrityError:
- # This exception is thrown if the email address is already in the database
- # To do: Perhaps put a link to sign in using an existing account here
- self.errors.append("An account with this email address already exists. "
- "Click the button above to sign in using an existing account.")
- return
-
- logger.debug("Adding verification email to queue")
- #self.send_email_verification()
- VerificationEmail(self.new_user)
- logger.debug("Added verification email to queue")
-
- self.thank_you_mode = True
-
+ save_user(es, self.user.__dict__, self.user.user_id)
def set_password(password, user):
pwfields = Bunch()
@@ -361,7 +356,7 @@ class VerificationEmail(object):
verification_code = str(uuid.uuid4())
key = self.key_prefix + ":" + verification_code
- data = json.dumps(dict(id=user.id,
+ data = json.dumps(dict(id=user.user_id,
timestamp=timestamp())
)
@@ -378,23 +373,33 @@ 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, user):
+ 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 = json.dumps(dict(id=user.id,
- timestamp=timestamp())
- )
+ data = {
+ "verification_code": verification_code,
+ "email_address": toaddr,
+ "timestamp": timestamp()
+ }
+ es_save_data(es, self.key_prefix, "local", data, verification_code)
- 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)
+ 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):
@@ -429,38 +434,61 @@ def verify_email():
response.set_cookie(UserSession.cookie_name, session_id_signed)
return response
-@app.route("/n/password_reset")
+@app.route("/n/password_reset", methods=['GET'])
def password_reset():
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()
-
- return render_template("new_security/password_reset.html", user_encode=user_encode)
+ # user_encode = DecodeUser(ForgotPasswordEmail.key_prefix).reencode_standalone()
+ verification_code = request.args.get('code')
+ hmac = request.args.get('hm')
+ if verification_code:
+ code_details = get_item_by_unique_column(
+ es
+ , "verification_code"
+ , verification_code
+ , ForgotPasswordEmail.key_prefix
+ , "local")
+ if code_details:
+ user_details = get_user_by_unique_column(
+ es
+ , "email_address"
+ , code_details["email_address"])
+ 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")
+ return redirect(url_for("login"))#render_template("new_security/login_user.html", error=error)
@app.route("/n/password_reset_step2", methods=('POST',))
def password_reset_step2():
+ from utility.elasticsearch_tools import es
logger.debug("in password_reset request.url is:", request.url)
errors = []
+ user_id = request.form['user_encode']
- user_encode = request.form['user_encode']
- verification_code, separator, hmac = user_encode.partition(':')
-
- hmac_verified = actual_hmac_creation(verification_code)
logger.debug("locals are:", locals())
- assert hmac == hmac_verified, "Someone has been naughty"
-
- user = DecodeUser.actual_get_user(ForgotPasswordEmail.key_prefix, verification_code)
- logger.debug("user is:", user)
-
+ user = Bunch()
password = request.form['password']
-
set_password(password, user)
- db_session.commit()
+
+ es = get_elasticsearch_connection()
+ es.update(
+ index = "users"
+ , doc_type = "local"
+ , id = user_id
+ , body = {
+ "doc": {
+ "password": user.__dict__.get("password")
+ }
+ })
flash("Password changed successfully. You can now sign in.", "alert-info")
response = make_response(redirect(url_for('login')))
@@ -492,8 +520,85 @@ class DecodeUser(object):
@app.route("/n/login", methods=('GET', 'POST'))
def login():
lu = LoginUser()
- return lu.standard_login()
+ 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"])
+ es = get_elasticsearch_connection()
+ user_details = get_user_by_unique_column(es, "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(es, 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"))
+
+ es = get_elasticsearch_connection()
+ user_details = get_user_by_unique_column(es, "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(es, 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
@@ -501,27 +606,56 @@ class LoginUser(object):
def __init__(self):
self.remember_me = False
+ def oauth2_login(self, login_type, user_id):
+ """Login via an OAuth2 provider"""
+ es = get_elasticsearch_connection()
+ user_details = get_user_by_unique_column(es, "user_id", user_id)
+ if user_details:
+ user = model.User()
+ user.id = user_details["user_id"]
+ 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)
+ es = get_elasticsearch_connection()
if not params:
- return render_template("new_security/login_user.html")
+ from utility.tools import GITHUB_AUTH_URL, ORCID_AUTH_URL
+ external_login = None
+ if GITHUB_AUTH_URL or ORCID_AUTH_URL:
+ external_login={
+ "github": GITHUB_AUTH_URL,
+ "orcid": ORCID_AUTH_URL
+ }
+ assert(es is not None)
+ return render_template(
+ "new_security/login_user.html"
+ , external_login=external_login
+ , es_server=es.ping())
else:
- try:
- user = model.User.query.filter_by(email_address=params['email_address']).one()
- except sqlalchemy.orm.exc.NoResultFound:
- logger.debug("No account exists for that email address")
- valid = False
- user = None
- else:
+ 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)
+ 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)
@@ -549,7 +683,7 @@ class LoginUser(object):
else:
if user:
self.unsuccessful_login(user)
- flash("Invalid email-address or password. Please try again.", "alert-error")
+ flash("Invalid email-address or password. Please try again.", "alert-danger")
response = make_response(redirect(url_for('login')))
return response
@@ -558,7 +692,6 @@ class LoginUser(object):
"""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")
- print("IMPORT1:", import_collections)
response = make_response(redirect(url_for('index_page', import_collections=import_collections)))
if self.remember_me:
max_age = self.remember_time
@@ -589,8 +722,6 @@ class LoginUser(object):
else:
expire_time = THREE_DAYS
Redis.expire(key, expire_time)
- db_session.add(login_rec)
- db_session.commit()
return session_id_signed
def unsuccessful_login(self, user):
@@ -618,13 +749,16 @@ def forgot_password():
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)
+ user_details = get_user_by_unique_column(es, "email_address", email_address)
+ if user_details:
+ ForgotPasswordEmail(user_details["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)
return render_template("new_security/forgot_password_step2.html",
subject=ForgotPasswordEmail.subject)
@@ -691,16 +825,18 @@ def register():
params = request.form if request.form else request.args
+ params = params.to_dict(flat=True)
+ es = get_elasticsearch_connection()
+ params["es_connection"] = es
if params:
logger.debug("Attempting to register the user...")
result = RegisterUser(params)
errors = result.errors
- if result.thank_you_mode:
- assert not errors, "Errors while in thank you mode? That seems wrong..."
- return render_template("new_security/registered.html",
- subject=VerificationEmail.subject)
+ 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)
@@ -771,13 +907,21 @@ 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 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"):
+ from smtplib import SMTP
+ from utility.tools import SMTP_CONNECT, SMTP_USERNAME, SMTP_PASSWORD
+ server = SMTP(SMTP_CONNECT)
+ server.starttls()
+ server.login(SMTP_USERNAME, SMTP_PASSWORD)
+ server.sendmail(fromaddr, toaddr, msg)
+ server.quit()