From 17970272f4f6e6436083b8b58d8b767a2efb2ab6 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Mon, 9 Jun 2014 15:17:59 -0600 Subject: [PATCH 01/21] add helper functions to util.py --- qiita_db/exceptions.py | 12 +++++- qiita_db/util.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/qiita_db/exceptions.py b/qiita_db/exceptions.py index 1f7a95081..7a36d4b18 100644 --- a/qiita_db/exceptions.py +++ b/qiita_db/exceptions.py @@ -32,9 +32,19 @@ class QiitaDBConnectionError(QiitaDBError): pass +class QiitaDBDuplicateError(QiitaDBError): + """Exception when duplicating something in the database""" + pass + + +class QiitaDBColumnError(QiitaDBError): + """Exception when database column info missing or incorrect""" + pass + + class QiitaDBUnknownIDError(QiitaDBError): """Exception for error when an object does not exists in the DB""" def __init__(self, missing_id, table): super(QiitaDBUnknownIDError, self).__init__() self.args = ("The object with ID '%s' does not exists in table '%s" - % (missing_id, table)) + % (missing_id, table)) \ No newline at end of file diff --git a/qiita_db/util.py b/qiita_db/util.py index e330cb9fc..e22c7b5a4 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -1,5 +1,9 @@ #!/usr/bin/env python from __future__ import division +from random import choice +from string import ascii_letters, digits, punctuation + +from .exceptions import QiitaDBColumnError # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team. @@ -48,3 +52,83 @@ def scrub_data(s): ret = s.replace("'", "") ret = ret.replace(";", "") return ret + + +def create_rand_string(length, punct=True): + """Returns a string of random ascii characters + + Parameters: + length: int + Length of string to return + punct: bool, optional + Include punctiation as well as letters and numbers. Default True. + """ + chars = ''.join((ascii_letters, digits)) + if punct: + chars = ''.join((chars, punctuation)) + return ''.join(choice(chars) for i in xrange(length+1)) + + +def check_required_columns(conn_handler, keys, table): + """Makes sure all required columns in database table are in keys + + Parameters + ---------- + conn_handler: SQLConnectionHandler object + Previously opened connection to the database + keys: iterable + Holds the keys in the dictionary + table: str + name of the table to check required columns + + Raises + ------ + QiitaDBColumnError + If keys exist that are not in the table + RuntimeError + Unable to get columns from database + """ + sql = ("SELECT is_nullable, column_name FROM information_schema.columns " + "WHERE table_name = %s") + cols = conn_handler.execute_fetchall(sql, (table, )) + # Test needed because a user with certain permissions can query without + # error but be unable to get the column names + if len(cols) == 0: + raise RuntimeError("Unable to fetch column names for table %s" % table) + required = set(x[1] for x in cols if x[0] == 'NO') + # remove the table id column as required + required.remove("%s_id" % table) + if len(required.difference(keys)) > 0: + raise QiitaDBColumnError("Required keys missing: %s" % + required.difference(keys)) + + +def check_table_cols(conn_handler, keys, table): + """Makes sure all keys correspond to column headers in a table + + Parameters + ---------- + conn_handler: SQLConnectionHandler object + Previously opened connection to the database + keys: iterable + Holds the keys in the dictionary + table: str + name of the table to check column names + + Raises + ------ + QiitaDBColumnError + If a key is found that is not in table columns + RuntimeError + Unable to get columns from database + """ + sql = ("SELECT column_name FROM information_schema.columns WHERE " + "table_name = %s") + cols = [x[0] for x in conn_handler.execute_fetchall(sql, (table, ))] + # Test needed because a user with certain permissions can query without + # error but be unable to get the column names + if len(cols) == 0: + raise RuntimeError("Unable to fetch column names for table %s" % table) + if len(set(keys).difference(cols)) > 0: + raise QiitaDBColumnError("Non-database keys found: %s" % + set(keys).difference(cols)) From 03c940caffb38951d6447c571d08036b6324547d Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Mon, 9 Jun 2014 15:18:16 -0600 Subject: [PATCH 02/21] add bcrypt to setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1bfb133e4..e10c9bb41 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,6 @@ scripts=glob('scripts/*'), extras_require={'test': ["nose >= 0.10.1", "pep8"], 'doc': ["Sphinx >= 1.2.2", "sphinx-bootstrap-theme"]}, - install_requires=['psycopg2', 'click == 1.0', 'future'], + install_requires=['psycopg2', 'click == 1.0', 'future', 'bcrypt'], classifiers=classifiers ) From 46e8b776b73aae9adfe5a0c1687b955f07c44713 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Mon, 9 Jun 2014 16:17:53 -0600 Subject: [PATCH 03/21] Add hash_pw to qiita_core utils --- qiita_core/util.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qiita_core/util.py b/qiita_core/util.py index 5664f0ca8..eb2c44e7f 100644 --- a/qiita_core/util.py +++ b/qiita_core/util.py @@ -5,11 +5,37 @@ # # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- +from bcrypt import hashpw, gensalt from qiita_db.sql_connection import SQLConnectionHandler from qiita_db.make_environment import (LAYOUT_FP, INITIALIZE_FP, POPULATE_FP) from qiita_core.qiita_settings import qiita_config +def hash_pw(password, hashedpw=None): + """ Hashes password + + Parameters + ---------- + password: str + Plaintext password + hashedpw: str, optional + Previously hashed password for bcrypt to pull salt from. If not + given, salt generated before hash + + Returns + ------- + str + Hashed password + + Notes + ----- + Relies on bcrypt library to hash passwords, which stores the salt as + part of the hashed password. Don't need to actually store the salt + because of this. + """ + if hashedpw is None: + hashedpw = gensalt() + return hashpw(password, hashedpw) def build_test_database(setup_fn): """Decorator that initializes the test database with the schema and initial From 1c0db85390486f0071c320c615f98b1cf2a28046 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Mon, 9 Jun 2014 16:26:49 -0600 Subject: [PATCH 04/21] add properties and class methods --- qiita_db/user.py | 208 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 188 insertions(+), 20 deletions(-) diff --git a/qiita_db/user.py b/qiita_db/user.py index 2a0a05165..fa9d115dd 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -17,11 +17,16 @@ # # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- - from .base import QiitaObject -from .exceptions import QiitaDBNotImplementedError -LEVELS = {'admin', 'dev', 'superuser', 'user', 'guest'} +from ..qiita_core.util import hash_pw +from .exceptions import (QiitaDBNotImplementedError, + IncompetentQiitaDeveloperError, + QiitaDBDuplicateError, QiitaDBColumnError) +from .sql_connection import SQLConnectionHandler +from .util import create_rand_string, check_table_cols +from .study import Study +from .analysis import Analysis class User(QiitaObject): @@ -65,21 +70,105 @@ class User(QiitaObject): Removes a shared analysis from the user """ - @staticmethod - def create(email, password): + _table = "qiita_user" + _non_info = {"email", "user_level_id", "password", "user_verify_code", + "pass_reset_code", "pass_reset_timestamp"} + + @classmethod + def login(cls, email, password): + """Logs a user into the system + + Parameters + ---------- + email : str + The email of the user + password: str + The plaintext password of the user + + Returns + ------- + User object or None + Returns the User object corresponding to the login information + or None if incorrect login information + """ + # see if user exists + if not cls.exists(email): + return None + + # pull password out of database + conn_handler = SQLConnectionHandler() + sql = ("SELECT password FROM qiita.{0} WHERE " + "email = %s".format(cls._table)) + dbpass = conn_handler.execute_fetchone(sql, (email, ))[0] + + # verify password + hashed = hash_pw(password, dbpass) + return cls(email) if hashed == dbpass else None + + @classmethod + def exists(cls, email): + """Checks if a user exists on the database + + Parameters + ---------- + email : str + the email of the user + """ + conn_handler = SQLConnectionHandler() + + return conn_handler.execute_fetchone( + "SELECT EXISTS(SELECT * FROM qiita.{0} WHERE " + "email = %s)".format(cls._table), (email, ))[0] + + @classmethod + def create(cls, email, password, info=None): """Creates a new user on the database Parameters ---------- email : str - the email of the user - used for log in + The email of the user - used for log in password : - the password of the user + The plaintext password of the user + info: dict + other information for the user keyed to table column name """ - raise QiitaDBNotImplementedError() - - @staticmethod - def delete(id_): + if email == "": + raise IncompetentQiitaDeveloperError("Blank username given!") + if password == "": + raise IncompetentQiitaDeveloperError("Blank password given!") + + # make sure user does not already exist + if cls.exists(email): + raise QiitaDBDuplicateError("User %s already exists" % email) + + # make sure non-info columns arent passed in info dict + for key in info: + if key in cls._non_info: + raise QiitaDBColumnError("%s should not be passed in info!" % + key) + + # create email verification code and hashed password to insert + # add values to info + info["email"] = email + info["password"] = hash_pw(password) + info["verify_code"] = create_rand_string(20, punct=False) + + # make sure keys in info correspond to columns in table + conn_handler = SQLConnectionHandler() + check_table_cols(conn_handler, info, cls._table) + + # build info to insert making sure columns and data are in same order + # for sql insertion + columns = info.keys() + values = (info[col] for col in columns) + + sql = ("INSERT INTO qiita.%s (%s) VALUES (%s)" % + (cls._table, ','.join(columns), ','.join(['%s'] * len(values)))) + conn_handler.execute(sql, values) + + @classmethod + def delete(cls, id_): """Deletes the user `id` from the database Parameters @@ -89,15 +178,45 @@ def delete(id_): """ raise QiitaDBNotImplementedError() + + def _check_id(self, id_, conn_handler=None): + r"""Check that the provided ID actually exists on the database + + Parameters + ---------- + id_ : object + The ID to test + The connection handler object connected to the DB + + Notes + ----- + This function overwrites the base function, as sql layout doesn't + follow the same conventions done in the other tables. + """ + self._check_subclass() + + conn_handler = (conn_handler if conn_handler is not None + else SQLConnectionHandler()) + print ("SELECT EXISTS(SELECT * FROM qiita.qiita_user WHERE " + "email = %s)" % id_) + return conn_handler.execute_fetchone( + "SELECT EXISTS(SELECT * FROM qiita.qiita_user WHERE " + "email = %s)", (id_, ))[0] + + # ---properties--- + @property def email(self): """The email of the user""" - return self.Id + return self._id @property def level(self): """The level of privileges of the user""" - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("SELECT user_level_id from qiita.{0} WHERE " + "email = %s".format(self._table)) + return conn_handler.execute_fetchone(sql, (self._id, ))[0] @level.setter def level(self, level): @@ -105,15 +224,30 @@ def level(self, level): Parameters ---------- - level : {'admin', 'dev', 'superuser', 'user', 'guest'} + level : int The new level of the user + + Notes + ----- + the ints correspond to {1: 'admin', 2: 'dev', 3: 'superuser', + 4: 'user', 5: 'unverified', 6: 'guest'} """ - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = ("UPDATE qiita.qiita_user SET user_level_id = %s WHERE " + "email = %s".format(self._table)) + conn_handler.execute(sql, (level, self._id)) @property def info(self): """Dict with any other information attached to the user""" - raise QiitaDBNotImplementedError() + conn_handler = SQLConnectionHandler() + sql = "SELECT * from qiita.{0} WHERE email = %s".format(self._table) + # Need direct typecast from psycopg2 dict to standard dict + info = dict(conn_handler.execute_fetchall(sql, (self._id, ))) + # Remove non-info columns + for col in self._non_info: + info.pop(col) + return info @info.setter def info(self, info): @@ -123,17 +257,46 @@ def info(self, info): ---------- info : dict """ - raise QiitaDBNotImplementedError() + # make sure non-info columns aren't passed in info dict + for key in info: + if key in self._non_info: + raise QiitaDBColumnError("%s should not be passed in info!" % + key) + + # make sure keys in info correspond to columns in table + conn_handler = SQLConnectionHandler() + check_table_cols(conn_handler, info, self._table) + + # build sql command and data to update + sql_insert = [] + data = [] + # items used for py3 compatability + for key, val in info.items(): + sql_insert.append("{0} = %s".format(key)) + data.append(val) + data.append(self._id) + + sql = ("UPDATE qiita.{0} SET {1} WHERE " + "email = %s".format(self._table, ','.join(sql_insert))) + conn_handler.execute(sql, data) @property def private_studies(self): """Returns a list of private studies owned by the user""" - raise QiitaDBNotImplementedError() + sql = ("SELECT study_id FROM qiita.study WHERE study_status_id = 3 AND" + " email = %s".format(self._table)) + conn_handler = SQLConnectionHandler() + studies = conn_handler.execute_fetchall(sql, (self._id, )) + return [Study(s[0]) for s in studies] @property def shared_studies(self): """Returns a list of studies shared with the user""" - raise QiitaDBNotImplementedError() + sql = ("SELECT study_id FROM qiita.study_users WHERE " + "email = %s".format(self._table)) + conn_handler = SQLConnectionHandler() + studies = conn_handler.execute_fetchall(sql, (self._id, )) + return [Study(s[0]) for s in studies] @property def private_analyses(self): @@ -143,8 +306,13 @@ def private_analyses(self): @property def shared_analyses(self): """Returns a list of analyses shared with the user""" - raise QiitaDBNotImplementedError() + sql = ("SELECT analysis_id FROM qiita.analysis_users WHERE " + "email = %s".format(self._table)) + conn_handler = SQLConnectionHandler() + analyses = conn_handler.execute_fetchall(sql, (self._id, )) + return [Analysis(a[0]) for a in analyses] + # ---Functions--- def add_private_study(self, study): """Adds a new private study to the user From 06bfefb0ab37ecbc0891359a761f2cd3aeb3329e Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Mon, 9 Jun 2014 16:33:26 -0600 Subject: [PATCH 05/21] add public status for analyses, user private analyses property --- qiita_core/util.py | 2 ++ qiita_db/support_files/initialize.sql | 2 +- qiita_db/user.py | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/qiita_core/util.py b/qiita_core/util.py index eb2c44e7f..a00debc2d 100644 --- a/qiita_core/util.py +++ b/qiita_core/util.py @@ -11,6 +11,7 @@ from qiita_db.make_environment import (LAYOUT_FP, INITIALIZE_FP, POPULATE_FP) from qiita_core.qiita_settings import qiita_config + def hash_pw(password, hashedpw=None): """ Hashes password @@ -37,6 +38,7 @@ def hash_pw(password, hashedpw=None): hashedpw = gensalt() return hashpw(password, hashedpw) + def build_test_database(setup_fn): """Decorator that initializes the test database with the schema and initial test data and executes setup_fn diff --git a/qiita_db/support_files/initialize.sql b/qiita_db/support_files/initialize.sql index c2ddb9ce4..1bf05b41e 100644 --- a/qiita_db/support_files/initialize.sql +++ b/qiita_db/support_files/initialize.sql @@ -2,7 +2,7 @@ INSERT INTO qiita.user_level (name, description) VALUES ('admin', 'Can access and do all the things'), ('dev', 'Can access all data and info about errors'), ('superuser', 'Can see all studies, can run analyses'), ('user', 'Can see own and public data, can run analyses'), ('unverified', 'Email not verified'), ('guest', 'Can view & download public data'); -- Populate analysis_status table -INSERT INTO qiita.analysis_status (status) VALUES ('in_construction'), ('queued'), ('running'), ('completed'), ('error'); +INSERT INTO qiita.analysis_status (status) VALUES ('in_construction'), ('queued'), ('running'), ('completed'), ('error'), ('public'); -- Populate job_status table INSERT INTO qiita.job_status (status) VALUES ('queued'), ('running'), ('completed'), ('error'); diff --git a/qiita_db/user.py b/qiita_db/user.py index fa9d115dd..6cd7175d2 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -178,7 +178,6 @@ def delete(cls, id_): """ raise QiitaDBNotImplementedError() - def _check_id(self, id_, conn_handler=None): r"""Check that the provided ID actually exists on the database @@ -301,7 +300,11 @@ def shared_studies(self): @property def private_analyses(self): """Returns a list of private analyses owned by the user""" - raise QiitaDBNotImplementedError() + sql = ("Select analysis_id from qiita.analysis WHERE email = %s AND " + "analysis_status_id <> 6") + conn_handler = SQLConnectionHandler() + analyses = conn_handler.execute_fetchall(sql, (self._id, )) + return [Analysis(a[0]) for a in analyses] @property def shared_analyses(self): From 92398a08bb31f7017371c9bf331bfb58015d2786 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Mon, 9 Jun 2014 16:36:07 -0600 Subject: [PATCH 06/21] add _table to analysis --- qiita_db/analysis.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiita_db/analysis.py b/qiita_db/analysis.py index 085de70cc..c9bb0e186 100644 --- a/qiita_db/analysis.py +++ b/qiita_db/analysis.py @@ -43,6 +43,8 @@ class Analysis(QiitaStatusObject): """ + _table = "analysis" + @staticmethod def create(owner): """Creates a new analysis on the database From f0fbeaeb53466cb6078854b0916e0413a6688c4e Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 14:24:32 -0600 Subject: [PATCH 07/21] create functions --- qiita_db/user.py | 67 +++++++++++------------------------------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/qiita_db/user.py b/qiita_db/user.py index 6cd7175d2..f4759ff22 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -45,12 +45,6 @@ class User(QiitaObject): Methods ------- - add_private_study(study) - Adds a new private study to the user - - remove_private_study(study) - Removes a private study from the user - add_shared_study(study) Adds a new shared study to the user @@ -316,35 +310,17 @@ def shared_analyses(self): return [Analysis(a[0]) for a in analyses] # ---Functions--- - def add_private_study(self, study): - """Adds a new private study to the user - - Parameters - ---------- - study : - The study to be added to the private list - """ - raise QiitaDBNotImplementedError() - - def remove_private_study(self, study): - """Removes a private study from the user - - Parameters - ---------- - study : - The study to be removed from the private list - """ - raise QiitaDBNotImplementedError() - def add_shared_study(self, study): """Adds a new shared study to the user Parameters ---------- - study : + study : Study object The study to be added to the shared list """ - raise QiitaDBNotImplementedError() + sql = "INSERT INTO qiita.study_users (email, study_id) VALUES (%s, %s)" + conn_handler = SQLConnectionHandler() + conn_handler.execute(sql, (self._id, study.id)) def remove_shared_study(self, study): """Removes a shared study from the user @@ -354,37 +330,22 @@ def remove_shared_study(self, study): study : The study to be removed from the shared list """ - raise QiitaDBNotImplementedError() - - def add_private_analysis(self, analysis): - """Adds a new private analysis to the user - - Parameters - ---------- - analysis : - The analysis to be added to the private list - """ - raise QiitaDBNotImplementedError() - - def remove_private_analysis(self, analysis): - """Removes a private analysis from the user - - Parameters - ---------- - analysis : - The analysis to be removed from the private list - """ - raise QiitaDBNotImplementedError() + sql = ("DELETE FROM qiita.study_users WHERE email = %s") + conn_handler = SQLConnectionHandler() + conn_handler.execute(sql, (self._id, )) def add_shared_analysis(self, analysis): """Adds a new shared analysis to the user Parameters ---------- - analysis : + analysis : Analysis object The analysis to be added to the shared list """ - raise QiitaDBNotImplementedError() + sql = ("INSERT INTO qiita.analysis_users (email, study_id) VALUES " + "(%s, %s)") + conn_handler = SQLConnectionHandler() + conn_handler.execute(sql, (self._id, analysis.id)) def remove_shared_analysis(self, analysis): """Removes a shared analysis from the user @@ -394,4 +355,6 @@ def remove_shared_analysis(self, analysis): analysis : The analysis to be removed from the shared list """ - raise QiitaDBNotImplementedError() + sql = ("DELETE FROM qiita.analysis_users WHERE email = %s") + conn_handler = SQLConnectionHandler() + conn_handler.execute(sql, (self._id, )) From 64b899502456f06e4b2993ae83f6d930aec6c3d8 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:24:26 -0600 Subject: [PATCH 08/21] add analysis to test database data --- qiita_db/support_files/initialize.sql | 5 ++++- qiita_db/support_files/populate_test_db.sql | 24 +++++++++++++++++++++ qiita_db/test/test_setup.py | 22 ++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/qiita_db/support_files/initialize.sql b/qiita_db/support_files/initialize.sql index ad9779f3e..1f511bfe0 100644 --- a/qiita_db/support_files/initialize.sql +++ b/qiita_db/support_files/initialize.sql @@ -35,4 +35,7 @@ INSERT INTO qiita.required_sample_info_status (status) VALUES ('received'), ('in INSERT INTO qiita.filepath_type (filepath_type) VALUES ('raw_sequences'), ('raw_barcodes'), ('raw_spectra'), ('preprocessed_sequences'), ('preprocessed_sequences_qual'), ('biom'); -- Populate checksum_algorithm table -INSERT INTO qiita.checksum_algorithm (name) VALUES ('crc32') +INSERT INTO qiita.checksum_algorithm (name) VALUES ('crc32'); + +-- Populate commands available +INSERT INTO qiita.command (name, command) VALUES ('Summarize taxa through plots', 'summarize_taxa_through_plots'), ('Beta diversity through plots', 'beta_diversity_through_plots'); diff --git a/qiita_db/support_files/populate_test_db.sql b/qiita_db/support_files/populate_test_db.sql index d0ebdad3f..3f60eecac 100644 --- a/qiita_db/support_files/populate_test_db.sql +++ b/qiita_db/support_files/populate_test_db.sql @@ -309,3 +309,27 @@ INSERT INTO qiita.filepath (filepath, filepath_type_id, checksum, checksum_algor -- Insert (link) the processed data with the processed filepath INSERT INTO qiita.processed_filepath (processed_data_id, filepath_id) VALUES (1, 5); + +-- Insert filepath for analysis biom file and job results files +INSERT INTO qiita.filepath (filepath, filepath_type_id, checksum, checksum_algorithm_id) VALUES ('firstanalysis.biom', 1, '852952723', 1), ('job1result.txt', 1, '852952723', 1), ('job2result.txt', 1, '852952723', 1); + +-- Insert jobs +INSERT INTO qiita.job (data_type_id, job_status_id, command_id, options) VALUES (1, 3, 1, 'options1'), (1, 3, 2, 'options2'); + +-- Insert Analysis +INSERT INTO qiita.analysis (email, name, description, analysis_status_id, pmid) VALUES ('test@foo.bar', 'SomeAnalysis', 'A test analysis', 4, '121112'); + +-- Attach jobs to analysis +INSERT INTO qiita.analysis_job (analysis_id, job_id) VALUES (1, 1), (1, 2); + +-- Attach filepath to analysis +INSERT INTO qiita.analysis_filepath (analysis_id, filepath_id) VALUES (1, 8); + +-- Attach samples to analysis +INSERT INTO qiita.analysis_sample (analysis_id, processed_data_id, sample_id) VALUES (1,1,'SKB8.640193'), (1,1,'SKD8.640184'), (1,1,'SKB7.640196'), (1,1,'SKM9.640192'), (1,1,'SKM4.640180'); + +--Share analysis with shared user +INSERT INTO qiita.analysis_users (analysis_id, email) VALUES (1, 'shared@foo.bar'); + +-- Add job results +INSERT INTO qiita.job_results_filepath (job_id, filepath_id) VALUES (1, 9), (2, 10); diff --git a/qiita_db/test/test_setup.py b/qiita_db/test/test_setup.py index 85bc94a3b..5a9623868 100644 --- a/qiita_db/test/test_setup.py +++ b/qiita_db/test/test_setup.py @@ -47,7 +47,7 @@ def test_study_experimental_factor(self): self._check_count("qiita.study_experimental_factor", 1) def test_filepath(self): - self._check_count("qiita.filepath", 7) + self._check_count("qiita.filepath", 10) def test_raw_data(self): self._check_count("qiita.raw_data", 2) @@ -100,6 +100,26 @@ def test_processed_params_uclust(self): def test_processed_filepath(self): self._check_count("qiita.processed_filepath", 1) + def test_job(self): + self._check_count("qiita.job", 2) + + def test_analysis(self): + self._check_count("qiita.analysis", 1) + + def test_analysis_job(self): + self._check_count("qiita.analysis_job", 2) + + def test_analysis_filepath(self): + self._check_count("qiita.analysis_filepath", 1) + + def test_analysis_sample(self): + self._check_count("qiita.analysis_sample", 5) + + def test_analysis_users(self): + self._check_count("qiita.analysis_users", 1) + + def test_job_results_filepath(self): + self._check_count("qiita.job_results_filepath", 2) if __name__ == '__main__': main() From 31f3d3be48320e43a4ac63c7ed8d071846b59c60 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:25:08 -0600 Subject: [PATCH 09/21] move hash_pw, bugfixes --- qiita_core/util.py | 29 --------------------- qiita_db/user.py | 64 ++++++++++++++++++++++++++-------------------- qiita_db/util.py | 31 +++++++++++++++++++++- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/qiita_core/util.py b/qiita_core/util.py index a00debc2d..894766aa6 100644 --- a/qiita_core/util.py +++ b/qiita_core/util.py @@ -5,40 +5,11 @@ # # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- -from bcrypt import hashpw, gensalt - from qiita_db.sql_connection import SQLConnectionHandler from qiita_db.make_environment import (LAYOUT_FP, INITIALIZE_FP, POPULATE_FP) from qiita_core.qiita_settings import qiita_config -def hash_pw(password, hashedpw=None): - """ Hashes password - - Parameters - ---------- - password: str - Plaintext password - hashedpw: str, optional - Previously hashed password for bcrypt to pull salt from. If not - given, salt generated before hash - - Returns - ------- - str - Hashed password - - Notes - ----- - Relies on bcrypt library to hash passwords, which stores the salt as - part of the hashed password. Don't need to actually store the salt - because of this. - """ - if hashedpw is None: - hashedpw = gensalt() - return hashpw(password, hashedpw) - - def build_test_database(setup_fn): """Decorator that initializes the test database with the schema and initial test data and executes setup_fn diff --git a/qiita_db/user.py b/qiita_db/user.py index f4759ff22..53164cdb4 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -1,14 +1,31 @@ #!/usr/bin/env python from __future__ import division -""" -Objects for dealing with Qiita users +r""" +User object (:mod:`qiita_db.user`) +===================================================== + +.. currentmodule:: qiita_db.user -This modules provides the implementation of the User class. +This modules provides the implementation of the User class. This is used for +handling creation, deletion, and login of users, as well as retrieval of all +studies and analyses that are owned by or shared with the user. Classes ------- -- `User` -- A Qiita user class + +.. autosummary:: + :toctree: generated/ + + User + + +Examples +-------- +A user is created using an email and password. + +>>> user = User('email@test.com', 'password') + """ # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team. @@ -19,9 +36,9 @@ # ----------------------------------------------------------------------------- from .base import QiitaObject -from ..qiita_core.util import hash_pw +from .util import hash_pw +from qiita_core.exceptions import IncompetentQiitaDeveloperError from .exceptions import (QiitaDBNotImplementedError, - IncompetentQiitaDeveloperError, QiitaDBDuplicateError, QiitaDBColumnError) from .sql_connection import SQLConnectionHandler from .util import create_rand_string, check_table_cols @@ -137,16 +154,19 @@ def create(cls, email, password, info=None): raise QiitaDBDuplicateError("User %s already exists" % email) # make sure non-info columns arent passed in info dict - for key in info: - if key in cls._non_info: - raise QiitaDBColumnError("%s should not be passed in info!" % - key) + if info: + for key in info: + if key in cls._non_info: + raise QiitaDBColumnError("%s should not be passed in info!" + % key) + else: + info = {} # create email verification code and hashed password to insert # add values to info info["email"] = email info["password"] = hash_pw(password) - info["verify_code"] = create_rand_string(20, punct=False) + info["user_verify_code"] = create_rand_string(20, punct=False) # make sure keys in info correspond to columns in table conn_handler = SQLConnectionHandler() @@ -155,22 +175,12 @@ def create(cls, email, password, info=None): # build info to insert making sure columns and data are in same order # for sql insertion columns = info.keys() - values = (info[col] for col in columns) + values = [info[col] for col in columns] sql = ("INSERT INTO qiita.%s (%s) VALUES (%s)" % (cls._table, ','.join(columns), ','.join(['%s'] * len(values)))) conn_handler.execute(sql, values) - - @classmethod - def delete(cls, id_): - """Deletes the user `id` from the database - - Parameters - ---------- - id_ : - The object identifier - """ - raise QiitaDBNotImplementedError() + return cls(email) def _check_id(self, id_, conn_handler=None): r"""Check that the provided ID actually exists on the database @@ -190,8 +200,6 @@ def _check_id(self, id_, conn_handler=None): conn_handler = (conn_handler if conn_handler is not None else SQLConnectionHandler()) - print ("SELECT EXISTS(SELECT * FROM qiita.qiita_user WHERE " - "email = %s)" % id_) return conn_handler.execute_fetchone( "SELECT EXISTS(SELECT * FROM qiita.qiita_user WHERE " "email = %s)", (id_, ))[0] @@ -236,7 +244,7 @@ def info(self): conn_handler = SQLConnectionHandler() sql = "SELECT * from qiita.{0} WHERE email = %s".format(self._table) # Need direct typecast from psycopg2 dict to standard dict - info = dict(conn_handler.execute_fetchall(sql, (self._id, ))) + info = dict(conn_handler.execute_fetchone(sql, (self._id, ))) # Remove non-info columns for col in self._non_info: info.pop(col) @@ -276,8 +284,8 @@ def info(self, info): @property def private_studies(self): """Returns a list of private studies owned by the user""" - sql = ("SELECT study_id FROM qiita.study WHERE study_status_id = 3 AND" - " email = %s".format(self._table)) + sql = ("SELECT study_id FROM qiita.study WHERE " + "email = %s".format(self._table)) conn_handler = SQLConnectionHandler() studies = conn_handler.execute_fetchall(sql, (self._id, )) return [Study(s[0]) for s in studies] diff --git a/qiita_db/util.py b/qiita_db/util.py index e22c7b5a4..95d848486 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -3,6 +3,8 @@ from random import choice from string import ascii_letters, digits, punctuation +from bcrypt import hashpw, gensalt + from .exceptions import QiitaDBColumnError # ----------------------------------------------------------------------------- @@ -66,7 +68,34 @@ def create_rand_string(length, punct=True): chars = ''.join((ascii_letters, digits)) if punct: chars = ''.join((chars, punctuation)) - return ''.join(choice(chars) for i in xrange(length+1)) + return ''.join(choice(chars) for i in xrange(length)) + + +def hash_pw(password, hashedpw=None): + """ Hashes password + + Parameters + ---------- + password: str + Plaintext password + hashedpw: str, optional + Previously hashed password for bcrypt to pull salt from. If not + given, salt generated before hash + + Returns + ------- + str + Hashed password + + Notes + ----- + Relies on bcrypt library to hash passwords, which stores the salt as + part of the hashed password. Don't need to actually store the salt + because of this. + """ + if hashedpw is None: + hashedpw = gensalt() + return hashpw(password, hashedpw) def check_required_columns(conn_handler, keys, table): From 2fd779829168657ff70879a6b32700663ceab10e Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:25:22 -0600 Subject: [PATCH 10/21] initial tests --- qiita_db/test/test_user.py | 190 +++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 qiita_db/test/test_user.py diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py new file mode 100644 index 000000000..b9535270a --- /dev/null +++ b/qiita_db/test/test_user.py @@ -0,0 +1,190 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2014--, The Qiita Development Team. +# +# Distributed under the terms of the BSD 3-clause License. +# +# The full license is in the file LICENSE, distributed with this software. +# ----------------------------------------------------------------------------- + +from unittest import TestCase, main + +from qiita_core.exceptions import IncompetentQiitaDeveloperError +from qiita_core.util import qiita_test_checker +from qiita_db.user import User +from qiita_db.study import Study +from qiita_db.sql_connection import SQLConnectionHandler +from qiita_db.exceptions import QiitaDBDuplicateError, QiitaDBColumnError + + +@qiita_test_checker() +class SetupTest(TestCase): + """Tests that the test database have been successfully populated""" + + def setUp(self): + self.conn = SQLConnectionHandler() + self.user = User('admin@foo.bar') + + self.userinfo = { + 'name': 'Dude', + 'affiliation': 'Nowhere University', + 'address': '123 fake st, Apt 0, Faketown, CO 80302', + 'phone': '111-222-3344' + } + + def test_create_user(self): + user = User.create('new@test.bar', 'password') + self.assertEqual(user.id, 'new@test.bar') + sql = "SELECT * from qiita.qiita_user WHERE email = 'new@test.bar'" + obs = self.conn.execute_fetchall(sql) + self.assertEqual(len(obs), 1) + obs = dict(obs[0]) + exp = { + 'password': '', + 'name': None, + 'pass_reset_timestamp': None, + 'affiliation': None, + 'pass_reset_code': None, + 'phone': None, + 'user_verify_code': '', + 'address': None, + 'user_level_id': 5, + 'email': 'new@test.bar'} + for key in exp: + # user_verify_code and password seed randomly generated so just + # making sure they exist and is correct length + if key == 'user_verify_code': + self.assertEqual(len(obs[key]), 20) + elif key == "password": + self.assertEqual(len(obs[key]), 60) + else: + self.assertEqual(obs[key], exp[key]) + + def test_create_user_info(self): + user = User.create('new@test.bar', 'password', self.userinfo) + self.assertEqual(user.id, 'new@test.bar') + sql = "SELECT * from qiita.qiita_user WHERE email = 'new@test.bar'" + obs = self.conn.execute_fetchall(sql) + self.assertEqual(len(obs), 1) + obs = dict(obs[0]) + exp = { + 'password': '', + 'name': 'Dude', + 'affiliation': 'Nowhere University', + 'address': '123 fake st, Apt 0, Faketown, CO 80302', + 'phone': '111-222-3344', + 'pass_reset_timestamp': None, + 'pass_reset_code': None, + 'user_verify_code': '', + 'user_level_id': 5, + 'email': 'new@test.bar'} + for key in exp: + # user_verify_code and password seed randomly generated so just + # making sure they exist and is correct length + if key == 'user_verify_code': + self.assertEqual(len(obs[key]), 20) + elif key == "password": + self.assertEqual(len(obs[key]), 60) + else: + self.assertEqual(obs[key], exp[key]) + + def test_create_user_bad_info(self): + self.userinfo["pass_reset_code"] = "FAIL" + with self.assertRaises(QiitaDBColumnError): + User.create('new@test.bar', 'password', self.userinfo) + + def test_create_user_not_info(self): + self.userinfo["BADTHING"] = "FAIL" + with self.assertRaises(QiitaDBColumnError): + User.create('new@test.bar', 'password', self.userinfo) + + def test_create_user_duplicate(self): + with self.assertRaises(QiitaDBDuplicateError): + User.create('test@foo.bar', 'password') + + def test_create_user_blank_email(self): + with self.assertRaises(IncompetentQiitaDeveloperError): + User.create('', 'password') + + def test_create_user_blank_password(self): + with self.assertRaises(IncompetentQiitaDeveloperError): + User.create('new@test.com', '') + + def test_login(self): + self.assertEqual(User.login("test@foo.bar", "password"), + User("test@foo.bar")) + + def test_login_incorrect_user(self): + self.assertEqual(User.login("notexist@foo.bar", "password"), + None) + + def test_login_incorrect_password(self): + self.assertEqual(User.login("test@foo.bar", "WRONG"), + None) + + def test_exists(self): + self.assertEqual(User.exists("test@foo.bar"), True) + + def test_exists_notindb(self): + self.assertEqual(User.exists("notexist@foo.bar"), False) + + def test_get_email(self): + self.assertEqual(self.user.email, 'admin@foo.bar') + + def test_get_level(self): + self.assertEqual(self.user.level, 4) + + def test_set_level(self): + self.user.level = 2 + self.assertEqual(self.user.level, 2) + + def test_get_info(self): + expinfo = { + 'name': 'Admin', + 'affiliation': 'Owner University', + 'address': '312 noname st, Apt K, Nonexistantown, CO 80302', + 'phone': '222-444-6789' + } + self.assertEqual(self.user.info, expinfo) + + def test_set_info(self): + self.user.info = self.userinfo + self.assertEqual(self.user.info, self.userinfo) + + def test_set_info_not_info(self): + self.userinfo["email"] = "FAIL" + with self.assertRaises(QiitaDBColumnError): + self.user = self.userinfo + + def test_set_info_bad_info(self): + self.userinfo["BADTHING"] = "FAIL" + with self.assertRaises(QiitaDBColumnError): + self.user = self.userinfo + + def test_get_private_studies(self): + user = User('test@foo.bar') + self.assertEqual(user.private_studies, [Study(1)]) + + def test_get_shared_studies(self): + user = User('shared@foo.bar') + self.assertEqual(user.shared_studies, [Study(1)]) + + def test_get_private_analyses(self): + self.assertEqual(self.user.private_analyses, []) + + def test_get_shared_analyses(self): + self.assertEqual(self.user.shared_analyses, []) + + def test_add_shared_study(self): + self.user.add_shared_study(Study(1)) + self.assertEqual(self.user.shared_studies, [Study(1)]) + + def test_remove_shared_study(self): + user = User('shared@foo.bar') + user.remove_shared_study(Study(1)) + self.assertEqual(user.shared_studies, []) + + + # TODO test analysis functions and the get/set analyses properly + +if __name__ == "__main__": + main() \ No newline at end of file From 7ed0c77a3a4b57820bd632767ca8b941dd7f796e Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:34:10 -0600 Subject: [PATCH 11/21] fix filepath for analysis --- qiita_db/support_files/populate_test_db.sql | 8 ++++---- qiita_db/test/test_setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiita_db/support_files/populate_test_db.sql b/qiita_db/support_files/populate_test_db.sql index 3f60eecac..6fcaa4340 100644 --- a/qiita_db/support_files/populate_test_db.sql +++ b/qiita_db/support_files/populate_test_db.sql @@ -310,8 +310,8 @@ INSERT INTO qiita.filepath (filepath, filepath_type_id, checksum, checksum_algor -- Insert (link) the processed data with the processed filepath INSERT INTO qiita.processed_filepath (processed_data_id, filepath_id) VALUES (1, 5); --- Insert filepath for analysis biom file and job results files -INSERT INTO qiita.filepath (filepath, filepath_type_id, checksum, checksum_algorithm_id) VALUES ('firstanalysis.biom', 1, '852952723', 1), ('job1result.txt', 1, '852952723', 1), ('job2result.txt', 1, '852952723', 1); +-- Insert filepath for job results files +INSERT INTO qiita.filepath (filepath, filepath_type_id, checksum, checksum_algorithm_id) VALUES ('job1result.txt', 1, '852952723', 1), ('job2result.txt', 1, '852952723', 1); -- Insert jobs INSERT INTO qiita.job (data_type_id, job_status_id, command_id, options) VALUES (1, 3, 1, 'options1'), (1, 3, 2, 'options2'); @@ -323,7 +323,7 @@ INSERT INTO qiita.analysis (email, name, description, analysis_status_id, pmid) INSERT INTO qiita.analysis_job (analysis_id, job_id) VALUES (1, 1), (1, 2); -- Attach filepath to analysis -INSERT INTO qiita.analysis_filepath (analysis_id, filepath_id) VALUES (1, 8); +INSERT INTO qiita.analysis_filepath (analysis_id, filepath_id) VALUES (1, 7); -- Attach samples to analysis INSERT INTO qiita.analysis_sample (analysis_id, processed_data_id, sample_id) VALUES (1,1,'SKB8.640193'), (1,1,'SKD8.640184'), (1,1,'SKB7.640196'), (1,1,'SKM9.640192'), (1,1,'SKM4.640180'); @@ -332,4 +332,4 @@ INSERT INTO qiita.analysis_sample (analysis_id, processed_data_id, sample_id) VA INSERT INTO qiita.analysis_users (analysis_id, email) VALUES (1, 'shared@foo.bar'); -- Add job results -INSERT INTO qiita.job_results_filepath (job_id, filepath_id) VALUES (1, 9), (2, 10); +INSERT INTO qiita.job_results_filepath (job_id, filepath_id) VALUES (1, 8), (2, 9); diff --git a/qiita_db/test/test_setup.py b/qiita_db/test/test_setup.py index 5a9623868..234a4b6a7 100644 --- a/qiita_db/test/test_setup.py +++ b/qiita_db/test/test_setup.py @@ -47,7 +47,7 @@ def test_study_experimental_factor(self): self._check_count("qiita.study_experimental_factor", 1) def test_filepath(self): - self._check_count("qiita.filepath", 10) + self._check_count("qiita.filepath", 9) def test_raw_data(self): self._check_count("qiita.raw_data", 2) From e2b7195b6f968d57d60c1ab3f563fd38824e100a Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:49:50 -0600 Subject: [PATCH 12/21] finishing up tests --- qiita_db/test/test_user.py | 14 ++++++++++++-- qiita_db/user.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index b9535270a..112f026e2 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -12,6 +12,7 @@ from qiita_core.util import qiita_test_checker from qiita_db.user import User from qiita_db.study import Study +from qiita_db.analysis import Analysis from qiita_db.sql_connection import SQLConnectionHandler from qiita_db.exceptions import QiitaDBDuplicateError, QiitaDBColumnError @@ -153,12 +154,12 @@ def test_set_info(self): def test_set_info_not_info(self): self.userinfo["email"] = "FAIL" with self.assertRaises(QiitaDBColumnError): - self.user = self.userinfo + self.user.info = self.userinfo def test_set_info_bad_info(self): self.userinfo["BADTHING"] = "FAIL" with self.assertRaises(QiitaDBColumnError): - self.user = self.userinfo + self.user.info = self.userinfo def test_get_private_studies(self): user = User('test@foo.bar') @@ -183,6 +184,15 @@ def test_remove_shared_study(self): user.remove_shared_study(Study(1)) self.assertEqual(user.shared_studies, []) + def test_add_shared_analysis(self): + self.user.add_shared_analysis(Analysis(1)) + self.assertEqual(self.user.shared_analyses, [Analysis(1)]) + + def test_remove_shared_analysis(self): + user = User('shared@foo.bar') + user.remove_shared_analysis(Analysis(1)) + self.assertEqual(user.shared_analyses, []) + # TODO test analysis functions and the get/set analyses properly diff --git a/qiita_db/user.py b/qiita_db/user.py index 53164cdb4..3add72e08 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -350,7 +350,7 @@ def add_shared_analysis(self, analysis): analysis : Analysis object The analysis to be added to the shared list """ - sql = ("INSERT INTO qiita.analysis_users (email, study_id) VALUES " + sql = ("INSERT INTO qiita.analysis_users (email, analysis_id) VALUES " "(%s, %s)") conn_handler = SQLConnectionHandler() conn_handler.execute(sql, (self._id, analysis.id)) From abe290460e89b52d9d0f7cd4af453526189ad7fb Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:52:10 -0600 Subject: [PATCH 13/21] PEP8 in tests --- qiita_db/test/test_user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 112f026e2..1c33d7011 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -194,7 +194,5 @@ def test_remove_shared_analysis(self): self.assertEqual(user.shared_analyses, []) - # TODO test analysis functions and the get/set analyses properly - if __name__ == "__main__": main() \ No newline at end of file From 252050d18ecb57075af30eeb8f05d577502fec93 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 17:58:30 -0600 Subject: [PATCH 14/21] fix utf-8 for py3 --- qiita_db/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiita_db/util.py b/qiita_db/util.py index 95d848486..aa35f355a 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -95,7 +95,7 @@ def hash_pw(password, hashedpw=None): """ if hashedpw is None: hashedpw = gensalt() - return hashpw(password, hashedpw) + return hashpw(password.encode('utf-8'), hashedpw.encode('utf-8')) def check_required_columns(conn_handler, keys, table): From 47d1cd7e23f1e8350735460e080623c5fc20afcd Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 18:14:06 -0600 Subject: [PATCH 15/21] python 3 hash workaround --- qiita_db/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qiita_db/util.py b/qiita_db/util.py index aa35f355a..3c6eb84be 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -95,7 +95,12 @@ def hash_pw(password, hashedpw=None): """ if hashedpw is None: hashedpw = gensalt() - return hashpw(password.encode('utf-8'), hashedpw.encode('utf-8')) + #python 3 workaround for bcrypt + if isinstance(password, bytes): + password = password.decode('utf-8') + if isinstance(hashedpw, bytes): + hashedpw = hashedpw.decode('utf-8') + return hashpw(password, hashedpw) def check_required_columns(conn_handler, keys, table): From 348d42eba6b412331e5e8ae0aea72acf1be4f9d3 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Tue, 10 Jun 2014 19:20:24 -0600 Subject: [PATCH 16/21] encode/decode for hash strings --- qiita_db/user.py | 3 +-- qiita_db/util.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/qiita_db/user.py b/qiita_db/user.py index 3add72e08..5b6c1aa8a 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -38,8 +38,7 @@ from .util import hash_pw from qiita_core.exceptions import IncompetentQiitaDeveloperError -from .exceptions import (QiitaDBNotImplementedError, - QiitaDBDuplicateError, QiitaDBColumnError) +from .exceptions import QiitaDBDuplicateError, QiitaDBColumnError from .sql_connection import SQLConnectionHandler from .util import create_rand_string, check_table_cols from .study import Study diff --git a/qiita_db/util.py b/qiita_db/util.py index 3c6eb84be..0f77bc005 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -68,7 +68,7 @@ def create_rand_string(length, punct=True): chars = ''.join((ascii_letters, digits)) if punct: chars = ''.join((chars, punctuation)) - return ''.join(choice(chars) for i in xrange(length)) + return ''.join(choice(chars) for i in range(length)) def hash_pw(password, hashedpw=None): @@ -93,14 +93,16 @@ def hash_pw(password, hashedpw=None): part of the hashed password. Don't need to actually store the salt because of this. """ + # all the encode/decode as a python 3 workaround for bcrypt if hashedpw is None: hashedpw = gensalt() - #python 3 workaround for bcrypt - if isinstance(password, bytes): - password = password.decode('utf-8') - if isinstance(hashedpw, bytes): - hashedpw = hashedpw.decode('utf-8') - return hashpw(password, hashedpw) + else: + hashedpw = hashedpw.encode('utf-8') + password = password.encode('utf-8') + output = hashpw(password, hashedpw) + if isinstance(output, bytes): + output = output.decode("utf-8") + return output def check_required_columns(conn_handler, keys, table): From bf2734968e9f12ccaf4ff969253931c960d9a431 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Wed, 11 Jun 2014 11:41:28 -0600 Subject: [PATCH 17/21] taking care of @josenavas comments --- qiita_core/exceptions.py | 10 ++ qiita_db/exceptions.py | 6 +- qiita_db/support_files/initialize.sql | 2 +- qiita_db/test/test_user.py | 104 +++++++-------- qiita_db/user.py | 182 +++++++++++--------------- 5 files changed, 142 insertions(+), 162 deletions(-) diff --git a/qiita_core/exceptions.py b/qiita_core/exceptions.py index e30b57645..8bb50e035 100644 --- a/qiita_core/exceptions.py +++ b/qiita_core/exceptions.py @@ -43,3 +43,13 @@ class QiitaJobError(QiitaError): class QiitaStudyError(QiitaError): """Exception for error when handling with study objects""" pass + + +class IncorrectPasswordError(QiitaError): + """User passes wrong password""" + pass + + +class IncorrectEmailError(QiitaError): + """Email fails validation""" + pass diff --git a/qiita_db/exceptions.py b/qiita_db/exceptions.py index 7a36d4b18..1e4c4a5b8 100644 --- a/qiita_db/exceptions.py +++ b/qiita_db/exceptions.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -from __future__ import division - # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team. # @@ -9,6 +6,7 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- +from __future__ import division from qiita_core.exceptions import QiitaError @@ -47,4 +45,4 @@ class QiitaDBUnknownIDError(QiitaDBError): def __init__(self, missing_id, table): super(QiitaDBUnknownIDError, self).__init__() self.args = ("The object with ID '%s' does not exists in table '%s" - % (missing_id, table)) \ No newline at end of file + % (missing_id, table)) diff --git a/qiita_db/support_files/initialize.sql b/qiita_db/support_files/initialize.sql index 1f511bfe0..b64865638 100644 --- a/qiita_db/support_files/initialize.sql +++ b/qiita_db/support_files/initialize.sql @@ -38,4 +38,4 @@ INSERT INTO qiita.filepath_type (filepath_type) VALUES ('raw_sequences'), ('raw_ INSERT INTO qiita.checksum_algorithm (name) VALUES ('crc32'); -- Populate commands available -INSERT INTO qiita.command (name, command) VALUES ('Summarize taxa through plots', 'summarize_taxa_through_plots'), ('Beta diversity through plots', 'beta_diversity_through_plots'); +INSERT INTO qiita.command (name, command) VALUES ('Summarize taxa through plots', 'summarize_taxa_through_plots.py'), ('Beta diversity through plots', 'beta_diversity_through_plots.py'); diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 1c33d7011..37035eb5e 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -8,7 +8,8 @@ from unittest import TestCase, main -from qiita_core.exceptions import IncompetentQiitaDeveloperError +from qiita_core.exceptions import (IncompetentQiitaDeveloperError, + IncorrectEmailError, IncorrectPasswordError) from qiita_core.util import qiita_test_checker from qiita_db.user import User from qiita_db.study import Study @@ -18,8 +19,8 @@ @qiita_test_checker() -class SetupTest(TestCase): - """Tests that the test database have been successfully populated""" +class UserTest(TestCase): + """Tests the Uer object and all properties/methods""" def setUp(self): self.conn = SQLConnectionHandler() @@ -32,6 +33,17 @@ def setUp(self): 'phone': '111-222-3344' } + def _check_correct_info(self, obs, exp): + for key in exp: + # user_verify_code and password seed randomly generated so just + # making sure they exist and is correct length + if key == 'user_verify_code': + self.assertEqual(len(obs[key]), 20) + elif key == "password": + self.assertEqual(len(obs[key]), 60) + else: + self.assertEqual(obs[key], exp[key]) + def test_create_user(self): user = User.create('new@test.bar', 'password') self.assertEqual(user.id, 'new@test.bar') @@ -50,15 +62,9 @@ def test_create_user(self): 'address': None, 'user_level_id': 5, 'email': 'new@test.bar'} - for key in exp: - # user_verify_code and password seed randomly generated so just - # making sure they exist and is correct length - if key == 'user_verify_code': - self.assertEqual(len(obs[key]), 20) - elif key == "password": - self.assertEqual(len(obs[key]), 60) - else: - self.assertEqual(obs[key], exp[key]) + # explicit list needed for py3 + self.assertEqual(list(exp.keys()).sort(), list(obs.keys()).sort()) + self._check_correct_info(obs, exp) def test_create_user_info(self): user = User.create('new@test.bar', 'password', self.userinfo) @@ -78,15 +84,9 @@ def test_create_user_info(self): 'user_verify_code': '', 'user_level_id': 5, 'email': 'new@test.bar'} - for key in exp: - # user_verify_code and password seed randomly generated so just - # making sure they exist and is correct length - if key == 'user_verify_code': - self.assertEqual(len(obs[key]), 20) - elif key == "password": - self.assertEqual(len(obs[key]), 60) - else: - self.assertEqual(obs[key], exp[key]) + # explicit list needed for py3 + self.assertEqual(list(exp.keys()).sort(), list(obs.keys()).sort()) + self._check_correct_info(obs, exp) def test_create_user_bad_info(self): self.userinfo["pass_reset_code"] = "FAIL" @@ -102,12 +102,12 @@ def test_create_user_duplicate(self): with self.assertRaises(QiitaDBDuplicateError): User.create('test@foo.bar', 'password') - def test_create_user_blank_email(self): - with self.assertRaises(IncompetentQiitaDeveloperError): - User.create('', 'password') + def test_create_user_bad_email(self): + with self.assertRaises(IncorrectEmailError): + User.create('notanemail', 'password') - def test_create_user_blank_password(self): - with self.assertRaises(IncompetentQiitaDeveloperError): + def test_create_user_bad_password(self): + with self.assertRaises(IncorrectPasswordError): User.create('new@test.com', '') def test_login(self): @@ -115,28 +115,40 @@ def test_login(self): User("test@foo.bar")) def test_login_incorrect_user(self): - self.assertEqual(User.login("notexist@foo.bar", "password"), - None) + with self.assertRaises(IncorrectEmailError): + User.login("notexist@foo.bar", "password") def test_login_incorrect_password(self): - self.assertEqual(User.login("test@foo.bar", "WRONG"), - None) + with self.assertRaises(IncorrectPasswordError): + User.login("test@foo.bar", "WRONGPASSWORD") + + def test_login_invalid_password(self): + with self.assertRaises(IncorrectPasswordError): + User.login("test@foo.bar", "SHORT") def test_exists(self): - self.assertEqual(User.exists("test@foo.bar"), True) + self.assertTrue(User.exists("test@foo.bar")) def test_exists_notindb(self): - self.assertEqual(User.exists("notexist@foo.bar"), False) + self.assertFalse(User.exists("notexist@foo.bar")) + + def test_exists_invaid_email(self): + with self.assertRaises(IncorrectEmailError): + User.exists("notanemail@badformat") def test_get_email(self): self.assertEqual(self.user.email, 'admin@foo.bar') def test_get_level(self): - self.assertEqual(self.user.level, 4) + self.assertEqual(self.user.level, "user") def test_set_level(self): - self.user.level = 2 - self.assertEqual(self.user.level, 2) + self.user.level = "admin" + self.assertEqual(self.user.level, "admin") + + def test_set_level_unknown(self): + with self.assertRaises(IncompetentQiitaDeveloperError): + self.user.level = "FAAAAAKE" def test_get_info(self): expinfo = { @@ -152,11 +164,13 @@ def test_set_info(self): self.assertEqual(self.user.info, self.userinfo) def test_set_info_not_info(self): + """Tests setting info with a non-allowed column""" self.userinfo["email"] = "FAIL" with self.assertRaises(QiitaDBColumnError): self.user.info = self.userinfo def test_set_info_bad_info(self): + """Test setting info with a key not in the table""" self.userinfo["BADTHING"] = "FAIL" with self.assertRaises(QiitaDBColumnError): self.user.info = self.userinfo @@ -175,24 +189,6 @@ def test_get_private_analyses(self): def test_get_shared_analyses(self): self.assertEqual(self.user.shared_analyses, []) - def test_add_shared_study(self): - self.user.add_shared_study(Study(1)) - self.assertEqual(self.user.shared_studies, [Study(1)]) - - def test_remove_shared_study(self): - user = User('shared@foo.bar') - user.remove_shared_study(Study(1)) - self.assertEqual(user.shared_studies, []) - - def test_add_shared_analysis(self): - self.user.add_shared_analysis(Analysis(1)) - self.assertEqual(self.user.shared_analyses, [Analysis(1)]) - - def test_remove_shared_analysis(self): - user = User('shared@foo.bar') - user.remove_shared_analysis(Analysis(1)) - self.assertEqual(user.shared_analyses, []) - if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/qiita_db/user.py b/qiita_db/user.py index 5b6c1aa8a..faf438e52 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -1,9 +1,6 @@ -#!/usr/bin/env python -from __future__ import division - r""" User object (:mod:`qiita_db.user`) -===================================================== +================================== .. currentmodule:: qiita_db.user @@ -19,13 +16,9 @@ User - Examples -------- -A user is created using an email and password. - ->>> user = User('email@test.com', 'password') - +TODO """ # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team. @@ -34,10 +27,14 @@ # # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- +from __future__ import division +from re import match, escape + from .base import QiitaObject from .util import hash_pw -from qiita_core.exceptions import IncompetentQiitaDeveloperError +from qiita_core.exceptions import (IncompetentQiitaDeveloperError, + IncorrectEmailError, IncorrectPasswordError) from .exceptions import QiitaDBDuplicateError, QiitaDBColumnError from .sql_connection import SQLConnectionHandler from .util import create_rand_string, check_table_cols @@ -61,23 +58,12 @@ class User(QiitaObject): Methods ------- - add_shared_study(study) - Adds a new shared study to the user - - remove_shared_study(study) - Removes a shared study from the user - - add_private_analysis(analysis) - Adds a new private analysis to the user - - remove_private_analysis(analysis) - Removes a private analysis from the user - - add_shared_analysis(analysis) - Adds a new shared analysis to the user - - remove_shared_analysis(analysis) - Removes a shared analysis from the user + add_shared_study + remove_shared_study + add_private_analysis + remove_private_analysis + add_shared_analysis + remove_shared_analysis """ _table = "qiita_user" @@ -103,7 +89,10 @@ def login(cls, email, password): """ # see if user exists if not cls.exists(email): - return None + raise IncorrectEmailError() + + if not validate_password(password): + raise IncorrectPasswordError("Password not valid!") # pull password out of database conn_handler = SQLConnectionHandler() @@ -113,7 +102,10 @@ def login(cls, email, password): # verify password hashed = hash_pw(password, dbpass) - return cls(email) if hashed == dbpass else None + if hashed == dbpass: + return cls(email) + else: + raise IncorrectPasswordError("Password not valid!") @classmethod def exists(cls, email): @@ -124,6 +116,8 @@ def exists(cls, email): email : str the email of the user """ + if not validate_email(email): + raise IncorrectEmailError() conn_handler = SQLConnectionHandler() return conn_handler.execute_fetchone( @@ -143,21 +137,20 @@ def create(cls, email, password, info=None): info: dict other information for the user keyed to table column name """ - if email == "": - raise IncompetentQiitaDeveloperError("Blank username given!") - if password == "": - raise IncompetentQiitaDeveloperError("Blank password given!") + # validate email and password for new user + if not validate_email(email): + raise IncorrectEmailError("Bad email given: %s" % email) + if not validate_password(password): + raise IncorrectPasswordError("Bad password given: %s" % password) # make sure user does not already exist if cls.exists(email): raise QiitaDBDuplicateError("User %s already exists" % email) - # make sure non-info columns arent passed in info dict + # make sure non-info columns aren't passed in info dict if info: - for key in info: - if key in cls._non_info: - raise QiitaDBColumnError("%s should not be passed in info!" - % key) + if cls._non_info.intersection(info): + raise QiitaDBColumnError("non info keys passed!") else: info = {} @@ -195,8 +188,6 @@ def _check_id(self, id_, conn_handler=None): This function overwrites the base function, as sql layout doesn't follow the same conventions done in the other tables. """ - self._check_subclass() - conn_handler = (conn_handler if conn_handler is not None else SQLConnectionHandler()) return conn_handler.execute_fetchone( @@ -214,8 +205,9 @@ def email(self): def level(self): """The level of privileges of the user""" conn_handler = SQLConnectionHandler() - sql = ("SELECT user_level_id from qiita.{0} WHERE " - "email = %s".format(self._table)) + sql = ("SELECT name from qiita.user_level WHERE user_level_id = " + "(SELECT user_level_id from qiita.{0} WHERE " + "email = %s)".format(self._table)) return conn_handler.execute_fetchone(sql, (self._id, ))[0] @level.setter @@ -224,16 +216,15 @@ def level(self, level): Parameters ---------- - level : int + level : {'admin', 'dev', 'superuser', 'user', 'guest'} The new level of the user - - Notes - ----- - the ints correspond to {1: 'admin', 2: 'dev', 3: 'superuser', - 4: 'user', 5: 'unverified', 6: 'guest'} """ + if level not in {'admin', 'dev', 'superuser', 'user', 'guest'}: + raise IncompetentQiitaDeveloperError("Unknown level given: %s" % + level) conn_handler = SQLConnectionHandler() - sql = ("UPDATE qiita.qiita_user SET user_level_id = %s WHERE " + sql = ("UPDATE qiita.{0} SET user_level_id = (SELECT user_level_id " + "from qiita.user_level WHERE name = %s) WHERE " "email = %s".format(self._table)) conn_handler.execute(sql, (level, self._id)) @@ -258,10 +249,8 @@ def info(self, info): info : dict """ # make sure non-info columns aren't passed in info dict - for key in info: - if key in self._non_info: - raise QiitaDBColumnError("%s should not be passed in info!" % - key) + if self._non_info.intersection(info): + raise QiitaDBColumnError("non info keys passed!") # make sure keys in info correspond to columns in table conn_handler = SQLConnectionHandler() @@ -286,8 +275,8 @@ def private_studies(self): sql = ("SELECT study_id FROM qiita.study WHERE " "email = %s".format(self._table)) conn_handler = SQLConnectionHandler() - studies = conn_handler.execute_fetchall(sql, (self._id, )) - return [Study(s[0]) for s in studies] + study_ids = conn_handler.execute_fetchall(sql, (self._id, )) + return [Study(s[0]) for s in study_ids] @property def shared_studies(self): @@ -295,8 +284,8 @@ def shared_studies(self): sql = ("SELECT study_id FROM qiita.study_users WHERE " "email = %s".format(self._table)) conn_handler = SQLConnectionHandler() - studies = conn_handler.execute_fetchall(sql, (self._id, )) - return [Study(s[0]) for s in studies] + study_ids = conn_handler.execute_fetchall(sql, (self._id, )) + return [Study(s[0]) for s in study_ids] @property def private_analyses(self): @@ -304,8 +293,8 @@ def private_analyses(self): sql = ("Select analysis_id from qiita.analysis WHERE email = %s AND " "analysis_status_id <> 6") conn_handler = SQLConnectionHandler() - analyses = conn_handler.execute_fetchall(sql, (self._id, )) - return [Analysis(a[0]) for a in analyses] + analysis_ids = conn_handler.execute_fetchall(sql, (self._id, )) + return [Analysis(a[0]) for a in analysis_ids] @property def shared_analyses(self): @@ -313,55 +302,42 @@ def shared_analyses(self): sql = ("SELECT analysis_id FROM qiita.analysis_users WHERE " "email = %s".format(self._table)) conn_handler = SQLConnectionHandler() - analyses = conn_handler.execute_fetchall(sql, (self._id, )) - return [Analysis(a[0]) for a in analyses] + analysis_ids = conn_handler.execute_fetchall(sql, (self._id, )) + return [Analysis(a[0]) for a in analysis_ids] - # ---Functions--- - def add_shared_study(self, study): - """Adds a new shared study to the user - Parameters - ---------- - study : Study object - The study to be added to the shared list - """ - sql = "INSERT INTO qiita.study_users (email, study_id) VALUES (%s, %s)" - conn_handler = SQLConnectionHandler() - conn_handler.execute(sql, (self._id, study.id)) +def validate_email(email): + """Makes sure email string has one @ and a period after the @ - def remove_shared_study(self, study): - """Removes a shared study from the user + Parameters + ---------- + email: str + email to validate - Parameters - ---------- - study : - The study to be removed from the shared list - """ - sql = ("DELETE FROM qiita.study_users WHERE email = %s") - conn_handler = SQLConnectionHandler() - conn_handler.execute(sql, (self._id, )) + Returns + ------- + bool + Whether or not the email is valid + """ + return True if match(r"[^@]+@[^@]+\.[^@]+", email) else False - def add_shared_analysis(self, analysis): - """Adds a new shared analysis to the user - Parameters - ---------- - analysis : Analysis object - The analysis to be added to the shared list - """ - sql = ("INSERT INTO qiita.analysis_users (email, analysis_id) VALUES " - "(%s, %s)") - conn_handler = SQLConnectionHandler() - conn_handler.execute(sql, (self._id, analysis.id)) +def validate_password(password): + """Validates a password is only ascii letters, numbers, or characters and + at least 8 characters - def remove_shared_analysis(self, analysis): - """Removes a shared analysis from the user + Parameters + ---------- + password: str + Password to validate - Parameters - ---------- - analysis : - The analysis to be removed from the shared list - """ - sql = ("DELETE FROM qiita.analysis_users WHERE email = %s") - conn_handler = SQLConnectionHandler() - conn_handler.execute(sql, (self._id, )) + Returns + ------- + bool + Whether or not the password is valid + + References + ----- + http://stackoverflow.com/questions/2990654/how-to-test-a-regex-password-in-python + """ + return True if match(r'[A-Za-z0-9@#$%^&+=]{8,}', password) else False From e0d526012ae7bb56ec85da157170b882c2c97ecc Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Wed, 11 Jun 2014 11:48:47 -0600 Subject: [PATCH 18/21] tests for new util functions --- qiita_db/test/test_util.py | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 qiita_db/test/test_util.py diff --git a/qiita_db/test/test_util.py b/qiita_db/test/test_util.py new file mode 100644 index 000000000..c74de4207 --- /dev/null +++ b/qiita_db/test/test_util.py @@ -0,0 +1,50 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2014--, The Qiita Development Team. +# +# Distributed under the terms of the BSD 3-clause License. +# +# The full license is in the file LICENSE, distributed with this software. +# ----------------------------------------------------------------------------- + +from unittest import TestCase, main + +from qiita_core.util import qiita_test_checker +from qiita_db.util import check_table_cols, check_required_columns +from qiita_db.sql_connection import SQLConnectionHandler +from qiita_db.exceptions import QiitaDBColumnError + + +@qiita_test_checker() +class DBUtilTests(TestCase): + def setUp(self): + self.conn_handler = SQLConnectionHandler() + self.table = 'study' + self.required = [ + 'number_samples_promised', 'study_title', 'mixs_compliant', + 'metadata_complete', 'study_description', 'first_contact', + 'reprocess', 'study_status_id', 'portal_type_id', + 'timeseries_type_id', 'study_alias', 'study_abstract', + 'principal_investigator_id', 'email', 'number_samples_collected'] + + def test_check_required_columns(self): + # Doesn't do anything if correct info passed, only errors if wrong info + check_required_columns(self.conn_handler, self.required, self.table) + + def test_check_required_columns_fail(self): + self.required.remove('study_title') + with self.assertRaises(QiitaDBColumnError): + check_required_columns(self.conn_handler, self.required, + self.table) + + def test_check_table_cols(self): + # Doesn't do anything if correct info passed, only errors if wrong info + check_table_cols(self.conn_handler, self.required, self.table) + + def test_check_table_cols_fail(self): + self.required.append('BADTHINGNOINHERE') + with self.assertRaises(QiitaDBColumnError): + check_table_cols(self.conn_handler, self.required, + self.table) + +if __name__ == "__main__": + main() From c8e216c7478a6a305adf4b4c0993d23f06565c8c Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Wed, 11 Jun 2014 13:22:18 -0600 Subject: [PATCH 19/21] more fixes --- qiita_db/test/test_user.py | 17 ++--------- qiita_db/user.py | 59 +++++++++++++++++++------------------- qiita_db/util.py | 22 +++++++------- 3 files changed, 43 insertions(+), 55 deletions(-) diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index 37035eb5e..de673be96 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -20,7 +20,7 @@ @qiita_test_checker() class UserTest(TestCase): - """Tests the Uer object and all properties/methods""" + """Tests the User object and all properties/methods""" def setUp(self): self.conn = SQLConnectionHandler() @@ -34,6 +34,7 @@ def setUp(self): } def _check_correct_info(self, obs, exp): + self.assertEqual(set(exp.keys()), set(obs.keys())) for key in exp: # user_verify_code and password seed randomly generated so just # making sure they exist and is correct length @@ -62,8 +63,6 @@ def test_create_user(self): 'address': None, 'user_level_id': 5, 'email': 'new@test.bar'} - # explicit list needed for py3 - self.assertEqual(list(exp.keys()).sort(), list(obs.keys()).sort()) self._check_correct_info(obs, exp) def test_create_user_info(self): @@ -84,11 +83,9 @@ def test_create_user_info(self): 'user_verify_code': '', 'user_level_id': 5, 'email': 'new@test.bar'} - # explicit list needed for py3 - self.assertEqual(list(exp.keys()).sort(), list(obs.keys()).sort()) self._check_correct_info(obs, exp) - def test_create_user_bad_info(self): + def test_create_user_column_not_allowed(self): self.userinfo["pass_reset_code"] = "FAIL" with self.assertRaises(QiitaDBColumnError): User.create('new@test.bar', 'password', self.userinfo) @@ -142,14 +139,6 @@ def test_get_email(self): def test_get_level(self): self.assertEqual(self.user.level, "user") - def test_set_level(self): - self.user.level = "admin" - self.assertEqual(self.user.level, "admin") - - def test_set_level_unknown(self): - with self.assertRaises(IncompetentQiitaDeveloperError): - self.user.level = "FAAAAAKE" - def test_get_info(self): expinfo = { 'name': 'Admin', diff --git a/qiita_db/user.py b/qiita_db/user.py index faf438e52..3181af648 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -28,14 +28,13 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- from __future__ import division -from re import match, escape +from re import match from .base import QiitaObject -from .util import hash_pw -from qiita_core.exceptions import (IncompetentQiitaDeveloperError, - IncorrectEmailError, IncorrectPasswordError) +from qiita_core.exceptions import IncorrectEmailError, IncorrectPasswordError from .exceptions import QiitaDBDuplicateError, QiitaDBColumnError +from .util import hash_password from .sql_connection import SQLConnectionHandler from .util import create_rand_string, check_table_cols from .study import Study @@ -67,6 +66,7 @@ class User(QiitaObject): """ _table = "qiita_user" + # The following columns are considered not part of the user info _non_info = {"email", "user_level_id", "password", "user_verify_code", "pass_reset_code", "pass_reset_timestamp"} @@ -83,13 +83,20 @@ def login(cls, email, password): Returns ------- - User object or None + User object Returns the User object corresponding to the login information - or None if incorrect login information + if correct login information + + Raises + ------ + IncorrectEmailError + Email passed is not a valid email + IncorrectPasswordError + Password passed is not correct for user """ # see if user exists if not cls.exists(email): - raise IncorrectEmailError() + raise IncorrectEmailError("Email not valid: %s" % email) if not validate_password(password): raise IncorrectPasswordError("Password not valid!") @@ -101,7 +108,7 @@ def login(cls, email, password): dbpass = conn_handler.execute_fetchone(sql, (email, ))[0] # verify password - hashed = hash_pw(password, dbpass) + hashed = hash_password(password, dbpass) if hashed == dbpass: return cls(email) else: @@ -135,13 +142,22 @@ def create(cls, email, password, info=None): password : The plaintext password of the user info: dict - other information for the user keyed to table column name + Other information for the user keyed to table column name + + Raises + ------ + IncorrectPasswordError + Password string given is not proper format + IncorrectEmailError + Email string given is not a valid email + QiitaDBDuplicateError + User already exists """ # validate email and password for new user if not validate_email(email): raise IncorrectEmailError("Bad email given: %s" % email) if not validate_password(password): - raise IncorrectPasswordError("Bad password given: %s" % password) + raise IncorrectPasswordError("Bad password given!") # make sure user does not already exist if cls.exists(email): @@ -150,14 +166,15 @@ def create(cls, email, password, info=None): # make sure non-info columns aren't passed in info dict if info: if cls._non_info.intersection(info): - raise QiitaDBColumnError("non info keys passed!") + raise QiitaDBColumnError("non info keys passed: %s" % + cls._non_info.intersection(info)) else: info = {} # create email verification code and hashed password to insert # add values to info info["email"] = email - info["password"] = hash_pw(password) + info["password"] = hash_password(password) info["user_verify_code"] = create_rand_string(20, punct=False) # make sure keys in info correspond to columns in table @@ -210,24 +227,6 @@ def level(self): "email = %s)".format(self._table)) return conn_handler.execute_fetchone(sql, (self._id, ))[0] - @level.setter - def level(self, level): - """ Sets the level of privileges of the user - - Parameters - ---------- - level : {'admin', 'dev', 'superuser', 'user', 'guest'} - The new level of the user - """ - if level not in {'admin', 'dev', 'superuser', 'user', 'guest'}: - raise IncompetentQiitaDeveloperError("Unknown level given: %s" % - level) - conn_handler = SQLConnectionHandler() - sql = ("UPDATE qiita.{0} SET user_level_id = (SELECT user_level_id " - "from qiita.user_level WHERE name = %s) WHERE " - "email = %s".format(self._table)) - conn_handler.execute(sql, (level, self._id)) - @property def info(self): """Dict with any other information attached to the user""" diff --git a/qiita_db/util.py b/qiita_db/util.py index 0f77bc005..e8431f553 100644 --- a/qiita_db/util.py +++ b/qiita_db/util.py @@ -1,12 +1,3 @@ -#!/usr/bin/env python -from __future__ import division -from random import choice -from string import ascii_letters, digits, punctuation - -from bcrypt import hashpw, gensalt - -from .exceptions import QiitaDBColumnError - # ----------------------------------------------------------------------------- # Copyright (c) 2014--, The Qiita Development Team. # @@ -15,6 +6,14 @@ # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- +from __future__ import division +from random import choice +from string import ascii_letters, digits, punctuation + +from bcrypt import hashpw, gensalt + +from .exceptions import QiitaDBColumnError + def quote_column_name(c): """Lowercases the string and puts double quotes around it @@ -59,7 +58,8 @@ def scrub_data(s): def create_rand_string(length, punct=True): """Returns a string of random ascii characters - Parameters: + Parameters + ---------- length: int Length of string to return punct: bool, optional @@ -71,7 +71,7 @@ def create_rand_string(length, punct=True): return ''.join(choice(chars) for i in range(length)) -def hash_pw(password, hashedpw=None): +def hash_password(password, hashedpw=None): """ Hashes password Parameters From 477caef6b0b767e56f51d6aea640a915758e4e07 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Wed, 11 Jun 2014 14:03:41 -0600 Subject: [PATCH 20/21] Add text for email error --- qiita_db/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiita_db/user.py b/qiita_db/user.py index 3181af648..3c3c5cef0 100644 --- a/qiita_db/user.py +++ b/qiita_db/user.py @@ -124,7 +124,7 @@ def exists(cls, email): the email of the user """ if not validate_email(email): - raise IncorrectEmailError() + raise IncorrectEmailError("Email string not valid: %s" % email) conn_handler = SQLConnectionHandler() return conn_handler.execute_fetchone( From 8f388b011c7bfa05447a1a69becb8a13c9e999d9 Mon Sep 17 00:00:00 2001 From: Joshua Shorenstein Date: Wed, 11 Jun 2014 14:21:20 -0600 Subject: [PATCH 21/21] test_create_user_not_existent_column --- qiita_db/test/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiita_db/test/test_user.py b/qiita_db/test/test_user.py index de673be96..d6412b3d8 100644 --- a/qiita_db/test/test_user.py +++ b/qiita_db/test/test_user.py @@ -90,7 +90,7 @@ def test_create_user_column_not_allowed(self): with self.assertRaises(QiitaDBColumnError): User.create('new@test.bar', 'password', self.userinfo) - def test_create_user_not_info(self): + def test_create_user_non_existent_column(self): self.userinfo["BADTHING"] = "FAIL" with self.assertRaises(QiitaDBColumnError): User.create('new@test.bar', 'password', self.userinfo)