From 2138e6b9d91bd3a109e202b9ad2029a737f374e6 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 23 Aug 2013 16:57:19 -0500 Subject: Working on user registration and added nginx conf for my branch --- wqflask/other_config/nginx_conf/sam.conf | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 wqflask/other_config/nginx_conf/sam.conf diff --git a/wqflask/other_config/nginx_conf/sam.conf b/wqflask/other_config/nginx_conf/sam.conf new file mode 100644 index 00000000..3cc443a8 --- /dev/null +++ b/wqflask/other_config/nginx_conf/sam.conf @@ -0,0 +1,42 @@ +server { + # Modeled after http://flask.pocoo.org/docs/deploying/wsgi-standalone/ + listen 80; + + server_name sam.genenetwork.org; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + location ^~ /css/ { + root /gene/wqflask/wqflask/static/; + } + + location ^~ /javascript/ { + root /gene/wqflask/wqflask/static/; + } + +# location ^~ /image/ { +# root /gene/wqflask/wqflask/static/; +# } + + location ^~ /images/ { + root /gene/wqflask/wqflask/static/; + } + + ### New - added by Sam + #location ^~ /static/ { + # root /gene/wqflask/wqflask/static/; + #} + + location / { + proxy_pass http://127.0.0.1:5555/; + proxy_redirect off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_read_timeout 40m; + } +} + -- cgit v1.2.3 From f2e41174a7e2091e7e835b137c6c7e2874feda04 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 23 Aug 2013 17:38:55 -0500 Subject: Finally figured out where to put flask_security into the code Various registration template changes --- wqflask/flask_security/__init__.py | 4 ++-- wqflask/secure_server.py | 3 +++ wqflask/wqflask/model.py | 14 +++++++++++++- wqflask/wqflask/templates/admin/ind_user_manager.html | 10 ++++++++++ wqflask/wqflask/templates/security/register_user.html | 16 +++++++++++++++- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/wqflask/flask_security/__init__.py b/wqflask/flask_security/__init__.py index 297033c9..81e6c89e 100644 --- a/wqflask/flask_security/__init__.py +++ b/wqflask/flask_security/__init__.py @@ -12,8 +12,6 @@ __version__ = '1.6.0' -print "using internal flask security" - from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .decorators import auth_token_required, http_auth_required, \ @@ -23,3 +21,5 @@ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ from .signals import confirm_instructions_sent, password_reset, \ reset_password_instructions_sent, user_confirmed, user_registered from .utils import login_user, logout_user, url_for_security + +print "Using our own flask.ext.security" \ No newline at end of file diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index 2abfdb05..04b85663 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -25,6 +25,9 @@ app.logger.addHandler(file_handler) import logging_tree logging_tree.printout() +import sys +print("At startup, path is:", sys.path) + #print("app.config is:", app.config) if __name__ == '__main__': diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 5beba9ff..1f873545 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -3,6 +3,9 @@ from __future__ import print_function, division, absolute_import from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin +from flask_security.forms import TextField +from flask_security.forms import RegisterForm + from wqflask import app # Create database connection object @@ -46,6 +49,10 @@ class User(db.Model, UserMixin): id = db.Column(db.Integer(), primary_key=True) email = db.Column(db.String(255), unique=True) password = db.Column(db.String(255)) + + name = db.Column(db.Unicode(255)) + organization = db.Column(db.Unicode(255)) + active = db.Column(db.Boolean()) confirmed_at = db.Column(db.DateTime()) @@ -60,7 +67,12 @@ class User(db.Model, UserMixin): # Setup Flask-Security user_datastore = SQLAlchemyUserDatastore(db, User, Role) -security = Security(app, user_datastore) + +class ExtendedRegisterForm(RegisterForm): + name = TextField('name') + organization = TextField('organization') + +security = Security(app, user_datastore, register_form=ExtendedRegisterForm) db.metadata.create_all(db.engine) diff --git a/wqflask/wqflask/templates/admin/ind_user_manager.html b/wqflask/wqflask/templates/admin/ind_user_manager.html index 9776af0b..2fe4a002 100644 --- a/wqflask/wqflask/templates/admin/ind_user_manager.html +++ b/wqflask/wqflask/templates/admin/ind_user_manager.html @@ -30,6 +30,16 @@ Value --> + + + Name + {{ user.name }} + + + + Organization + {{ user.organization }} + diff --git a/wqflask/wqflask/templates/security/register_user.html b/wqflask/wqflask/templates/security/register_user.html index 3cd021b0..8e6908ff 100644 --- a/wqflask/wqflask/templates/security/register_user.html +++ b/wqflask/wqflask/templates/security/register_user.html @@ -28,7 +28,21 @@ {{ render_only_errors(register_user_form.email) }} + +
+ +
+ +
+
+
+ +
+ +
+
+
@@ -49,7 +63,7 @@
- +
-- cgit v1.2.3 From cd4d985e7142f92e18f8cf316c72073cb3609a85 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 28 Aug 2013 16:21:12 -0500 Subject: Various changes to track down what happens with security modules --- wqflask/flask_security/core.py | 1 + wqflask/flask_security/datastore.py | 1 + wqflask/flask_security/forms.py | 3 +++ wqflask/flask_security/registerable.py | 1 + wqflask/secure_server.py | 15 +++++++++++++++ 5 files changed, 21 insertions(+) diff --git a/wqflask/flask_security/core.py b/wqflask/flask_security/core.py index d794fad5..0f3a231f 100644 --- a/wqflask/flask_security/core.py +++ b/wqflask/flask_security/core.py @@ -207,6 +207,7 @@ def _get_serializer(app, name): def _get_state(app, datastore, **kwargs): for key, value in get_config(app).items(): + print "in _get_state [{}]: {}".format(key, value) kwargs[key.lower()] = value kwargs.update(dict( diff --git a/wqflask/flask_security/datastore.py b/wqflask/flask_security/datastore.py index f8c7218d..634399d9 100644 --- a/wqflask/flask_security/datastore.py +++ b/wqflask/flask_security/datastore.py @@ -157,6 +157,7 @@ class UserDatastore(object): """Creates and returns a new user from the given parameters.""" user = self.user_model(**self._prepare_create_user_args(**kwargs)) + print "in abstraced create_user, user is:", user return self.put(user) def delete_user(self, user): diff --git a/wqflask/flask_security/forms.py b/wqflask/flask_security/forms.py index e64e1502..4c1dc894 100644 --- a/wqflask/flask_security/forms.py +++ b/wqflask/flask_security/forms.py @@ -89,6 +89,8 @@ def valid_user_email(form, field): class Form(BaseForm): def __init__(self, *args, **kwargs): + print "in Form, args:", args + print "in Form, kwargs:", kwargs if current_app.testing: self.TIME_LIMIT = None super(Form, self).__init__(*args, **kwargs) @@ -148,6 +150,7 @@ class RegisterFormMixin(): def to_dict(form): def is_field_and_user_attr(member): + print "in ifaua:", member return isinstance(member, Field) and \ hasattr(_datastore.user_model, member.name) diff --git a/wqflask/flask_security/registerable.py b/wqflask/flask_security/registerable.py index 4e9f357d..4606c7c6 100644 --- a/wqflask/flask_security/registerable.py +++ b/wqflask/flask_security/registerable.py @@ -24,6 +24,7 @@ _datastore = LocalProxy(lambda: _security.datastore) def register_user(**kwargs): + print "in register_user kwargs:", kwargs confirmation_link, token = None, None kwargs['password'] = encrypt_password(kwargs['password']) user = _datastore.create_user(**kwargs) diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index 04b85663..b877c544 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -28,6 +28,21 @@ logging_tree.printout() import sys print("At startup, path is:", sys.path) + +######## +def tracefunc(frame, event, arg, indent=[0]): + if event == "call": + indent[0] += 2 + print("-" * indent[0] + "> call function", frame.f_code.co_name) + elif event == "return": + print("<" + "-" * indent[0], "exit function", frame.f_code.co_name) + indent[0] -= 2 + return tracefunc + +import sys +sys.settrace(tracefunc) +############## + #print("app.config is:", app.config) if __name__ == '__main__': -- cgit v1.2.3 From 8d61798c41f9ec77dea3231cbcf0957c7cadc533 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Sep 2013 15:31:22 -0500 Subject: Trying to trace through files --- wqflask/flask_security/forms.py | 6 ++++++ wqflask/secure_server.py | 19 ++----------------- wqflask/wqflask/model.py | 1 + wqflask/wqflask/tracer.py | 41 +++++++++++++++++++++++++++++++++++++++++ wqflask/wqflask/views.py | 5 +++++ 5 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 wqflask/wqflask/tracer.py diff --git a/wqflask/flask_security/forms.py b/wqflask/flask_security/forms.py index 4c1dc894..54677e77 100644 --- a/wqflask/flask_security/forms.py +++ b/wqflask/flask_security/forms.py @@ -89,6 +89,10 @@ def valid_user_email(form, field): class Form(BaseForm): def __init__(self, *args, **kwargs): + #print "importing tracer" + #from wqflask import tracer + #tracer.turn_on() + #print "imported tracer" print "in Form, args:", args print "in Form, kwargs:", kwargs if current_app.testing: @@ -154,7 +158,9 @@ class RegisterFormMixin(): return isinstance(member, Field) and \ hasattr(_datastore.user_model, member.name) + print("green:", vars(form)) fields = inspect.getmembers(form, is_field_and_user_attr) + print("fields:" ,vars(form)) return dict((key, value.data) for key, value in fields) diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index b877c544..697ebfe0 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -1,4 +1,4 @@ -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function from wqflask import app @@ -28,21 +28,6 @@ logging_tree.printout() import sys print("At startup, path is:", sys.path) - -######## -def tracefunc(frame, event, arg, indent=[0]): - if event == "call": - indent[0] += 2 - print("-" * indent[0] + "> call function", frame.f_code.co_name) - elif event == "return": - print("<" + "-" * indent[0], "exit function", frame.f_code.co_name) - indent[0] -= 2 - return tracefunc - -import sys -sys.settrace(tracefunc) -############## - #print("app.config is:", app.config) if __name__ == '__main__': @@ -51,4 +36,4 @@ if __name__ == '__main__': port=app.config['SERVER_PORT'], use_debugger=False, threaded=True, - use_reloader=True) + use_reloader=False) diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 1f873545..c1b8d060 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -70,6 +70,7 @@ user_datastore = SQLAlchemyUserDatastore(db, User, Role) class ExtendedRegisterForm(RegisterForm): name = TextField('name') + #print("name is:", name['_name'], vars(name)) organization = TextField('organization') security = Security(app, user_datastore, register_form=ExtendedRegisterForm) diff --git a/wqflask/wqflask/tracer.py b/wqflask/wqflask/tracer.py new file mode 100644 index 00000000..43a0f15e --- /dev/null +++ b/wqflask/wqflask/tracer.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division, print_function + +print("At top of tracer") + +import sys + +#################################################################################### + +# Originally based on http://stackoverflow.com/a/8315566 +def tracefunc(frame, event, arg, indent=[0]): + + func = dict(funcname = frame.f_code.co_name, + filename = frame.f_code.co_filename, + lineno = frame.f_lineno) + + #These are too common to bother printing... + too_common = ( + '/home/sam/ve27/local/lib/python2.7/site-packages/werkzeug/', + '/home/sam/ve27/local/lib/python2.7/site-packages/jinja2/', + ) + + + if func['filename'].startswith(too_common): + return tracefunc + + info = "{funcname} [{filename}: {lineno}]".format(**func) + + if event == "call": + indent[0] += 2 + #print("-" * indent[0] + "> call function", frame.f_code.co_name) + print("-" * indent[0] + "> call function:", info) + elif event == "return": + print("<" + "-" * indent[0], "exit function:", info) + indent[0] -= 2 + return tracefunc + +def turn_on(): + sys.settrace(tracefunc) + print("Tracing turned on!!!!") +#################################################################################### + diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index d57bbaa7..2b16dd77 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -50,6 +50,11 @@ from wqflask import user_manager @app.before_request def connect_db(): g.db = sqlalchemy.create_engine(app.config['DB_URI']) + +@app.before_request +def trace_it(): + from wqflask import tracer + tracer.turn_on() @app.route("/") def index_page(): -- cgit v1.2.3 From 0262c24989a1095b33911db4b82b5b5e578b9249 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 5 Sep 2013 15:42:33 -0500 Subject: Adjusting to using our own registariton package --- .../wqflask/templates/new_security/_scripts.html | 3 + .../wqflask/templates/new_security/login_user.html | 61 ++++++++++++++++++++ .../templates/new_security/register_user.html | 66 ++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 wqflask/wqflask/templates/new_security/_scripts.html create mode 100644 wqflask/wqflask/templates/new_security/login_user.html create mode 100644 wqflask/wqflask/templates/new_security/register_user.html diff --git a/wqflask/wqflask/templates/new_security/_scripts.html b/wqflask/wqflask/templates/new_security/_scripts.html new file mode 100644 index 00000000..5a453dca --- /dev/null +++ b/wqflask/wqflask/templates/new_security/_scripts.html @@ -0,0 +1,3 @@ + + diff --git a/wqflask/wqflask/templates/new_security/login_user.html b/wqflask/wqflask/templates/new_security/login_user.html new file mode 100644 index 00000000..360a6a68 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/login_user.html @@ -0,0 +1,61 @@ +
+ +

* Don't have an account?

+ +
+ Create a new account +
+ +
+ +

Already have an account?

+ +
Sign in here
+ +
+
+ + +
+ +
+ +
+
+ +
+ + +
+ + +
+
+ +
+ + +
+
+ + + +
+ + +
+
+ +
+
+ + +{% include "new_security/_scripts.html" %} + diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html new file mode 100644 index 00000000..d66365f4 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -0,0 +1,66 @@ +
+

* Already have an account?

+ +
+ Sign in using existing account +
+ +
+ +

Don't have an account?

+ +
Register here
+ +
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+{% include "security/_scripts.html" %} + -- cgit v1.2.3 From b6bf777ef977436472651727da4565e1ce04b0da Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 5 Sep 2013 18:34:46 -0500 Subject: Switched registration form to no longer be modal, but instead it's own page Added validation of form using parsley.js Added password strength indicator using zxcvbn --- wqflask/wqflask/static/new/css/parsley.css | 20 +++++++++++ .../static/new/javascript/password_strength.coffee | 33 +++++++++++++++++ .../static/new/javascript/password_strength.js | 42 ++++++++++++++++++++++ .../wqflask/static/new/js_external/parsley.min.js | 35 ++++++++++++++++++ .../static/new/js_external/zxcvbn/.gitignore | 2 ++ 5 files changed, 132 insertions(+) create mode 100644 wqflask/wqflask/static/new/css/parsley.css create mode 100644 wqflask/wqflask/static/new/javascript/password_strength.coffee create mode 100644 wqflask/wqflask/static/new/javascript/password_strength.js create mode 100644 wqflask/wqflask/static/new/js_external/parsley.min.js create mode 100644 wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore diff --git a/wqflask/wqflask/static/new/css/parsley.css b/wqflask/wqflask/static/new/css/parsley.css new file mode 100644 index 00000000..7d244579 --- /dev/null +++ b/wqflask/wqflask/static/new/css/parsley.css @@ -0,0 +1,20 @@ +/* Adapted from parsleyjs.org/documentation.html#parsleyclasses */ + +input.parsley-success, textarea.parsley-success { + color: #468847 !important; + background-color: #DFF0D8 !important; + border: 1px solid #D6E9C6 !important; +} +input.parsley-error, textarea.parsley-error { + color: #B94A48 !important; + background-color: #F2DEDE !important; + border: 1px solid #EED3D7 !important; +} +ul.parsley-error-list { + font-size: 11px; + margin: 2px; + list-style-type:none; +} +ul.parsley-error-list li { + line-height: 11px; +} \ No newline at end of file diff --git a/wqflask/wqflask/static/new/javascript/password_strength.coffee b/wqflask/wqflask/static/new/javascript/password_strength.coffee new file mode 100644 index 00000000..0bee5836 --- /dev/null +++ b/wqflask/wqflask/static/new/javascript/password_strength.coffee @@ -0,0 +1,33 @@ +$ -> + + + $("#password").keyup -> + passtext = $(this).val() + result = zxcvbn(passtext) + if passtext.length < 6 + $("#password_strength").html('') + $("#password_alert").fadeOut() + else + word = word_score(result.score) + crack_time = result.crack_time_display + if crack_time == "instant" + crack_time = "a second" + display = "This is #{word} password. It can be cracked in #{crack_time}." + $("#password_strength").html(display) + $("#password_alert").fadeIn() + + + + word_score = (num_score) -> + num_score = parseInt(num_score) + console.log("num_score is:", num_score) + mapping = + 0: "a terrible" + 1: "a bad" + 2: "a mediocre" + 3: "a good" + 4: "an excellent" + console.log("mapping is:", mapping) + result = mapping[num_score] + console.log("result is:", result) + return result \ No newline at end of file diff --git a/wqflask/wqflask/static/new/javascript/password_strength.js b/wqflask/wqflask/static/new/javascript/password_strength.js new file mode 100644 index 00000000..166e1125 --- /dev/null +++ b/wqflask/wqflask/static/new/javascript/password_strength.js @@ -0,0 +1,42 @@ +// Generated by CoffeeScript 1.6.1 +(function() { + + $(function() { + var word_score; + $("#password").keyup(function() { + var crack_time, display, passtext, result, word; + passtext = $(this).val(); + result = zxcvbn(passtext); + if (passtext.length < 6) { + $("#password_strength").html(''); + return $("#password_alert").fadeOut(); + } else { + word = word_score(result.score); + crack_time = result.crack_time_display; + if (crack_time === "instant") { + crack_time = "a second"; + } + display = "This is " + word + " password. It can be cracked in " + crack_time + "."; + $("#password_strength").html(display); + return $("#password_alert").fadeIn(); + } + }); + return word_score = function(num_score) { + var mapping, result; + num_score = parseInt(num_score); + console.log("num_score is:", num_score); + mapping = { + 0: "a terrible", + 1: "a bad", + 2: "a mediocre", + 3: "a good", + 4: "an excellent" + }; + console.log("mapping is:", mapping); + result = mapping[num_score]; + console.log("result is:", result); + return result; + }; + }); + +}).call(this); diff --git a/wqflask/wqflask/static/new/js_external/parsley.min.js b/wqflask/wqflask/static/new/js_external/parsley.min.js new file mode 100644 index 00000000..ab85c683 --- /dev/null +++ b/wqflask/wqflask/static/new/js_external/parsley.min.js @@ -0,0 +1,35 @@ +/* Parsley dist/parsley.min.js build version 1.1.18 http://parsleyjs.org */ +!function(d){var h=function(a){this.messages={defaultMessage:"This value seems to be invalid.",type:{email:"This value should be a valid email.",url:"This value should be a valid url.",urlstrict:"This value should be a valid url.",number:"This value should be a valid number.",digits:"This value should be digits.",dateIso:"This value should be a valid date (YYYY-MM-DD).",alphanum:"This value should be alphanumeric.",phone:"This value should be a valid phone number."},notnull:"This value should not be null.", +notblank:"This value should not be blank.",required:"This value is required.",regexp:"This value seems to be invalid.",min:"This value should be greater than or equal to %s.",max:"This value should be lower than or equal to %s.",range:"This value should be between %s and %s.",minlength:"This value is too short. It should have %s characters or more.",maxlength:"This value is too long. It should have %s characters or less.",rangelength:"This value length is invalid. It should be between %s and %s characters long.", +mincheck:"You must select at least %s choices.",maxcheck:"You must select %s choices or less.",rangecheck:"You must select between %s and %s choices.",equalto:"This value should be the same."};this.init(a)};h.prototype={constructor:h,validators:{notnull:function(a){return 0=b},maxlength:function(a,b){return a.length<=b},rangelength:function(a,b){return this.minlength(a,b[0])&&this.maxlength(a,b[1])}, +min:function(a,b){return Number(a)>=b},max:function(a,b){return Number(a)<=b},range:function(a,b){return a>=b[0]&&a<=b[1]},equalto:function(a,b,c){c.options.validateIfUnchanged=!0;return a===d(b).val()},remote:function(a,b,c){var f={},g={};f[c.$element.attr("name")]=a;"undefined"!==typeof c.options.remoteDatatype&&(g={dataType:c.options.remoteDatatype});var m=function(a,b){"undefined"!==typeof b&&("undefined"!==typeof c.Validator.messages.remote&&b!==c.Validator.messages.remote)&&d(c.ulError+" .remote").remove(); +c.updtConstraint({name:"remote",valid:a},b);c.manageValidationResult()},n=function(a){if("object"===typeof a)return a;try{a=d.parseJSON(a)}catch(b){}return a},e=function(a){return"object"===typeof a&&null!==a?"undefined"!==typeof a.error?a.error:"undefined"!==typeof a.message?a.message:null:null};d.ajax(d.extend({},{url:b,data:f,type:c.options.remoteMethod||"GET",success:function(a){a=n(a);m(1===a||!0===a||"object"===typeof a&&null!==a&&"undefined"!==typeof a.success,e(a))},error:function(a){a=n(a); +m(!1,e(a))}},g));return null},mincheck:function(a,b){return this.minlength(a,b)},maxcheck:function(a,b){return this.maxlength(a,b)},rangecheck:function(a,b){return this.rangelength(a,b)}},init:function(a){var b=a.validators;a=a.messages;for(var c in b)this.addValidator(c,b[c]);for(c in a)this.addMessage(c,a[c])},formatMesssage:function(a,b){if("object"===typeof b){for(var c in b)a=this.formatMesssage(a,b[c]);return a}return"string"===typeof a?a.replace(/%s/i,b):""},addValidator:function(a,b){this.validators[a]= +b},addMessage:function(a,b,c){if("undefined"!==typeof c&&!0===c)this.messages.type[a]=b;else if("type"===a)for(var d in b)this.messages.type[d]=b[d];else this.messages[a]=b}};var j=function(a,b,c){this.options=b;this.Validator=new h(b);if("ParsleyFieldMultiple"===c)return this;this.init(a,c||"ParsleyField")};j.prototype={constructor:j,init:function(a,b){this.type=b;this.valid=!0;this.element=a;this.validatedOnce=!1;this.$element=d(a);this.val=this.$element.val();this.isRequired=!1;this.constraints= +{};"undefined"===typeof this.isRadioOrCheckbox&&(this.isRadioOrCheckbox=!1,this.hash=this.generateHash(),this.errorClassHandler=this.options.errors.classHandler(a,this.isRadioOrCheckbox)||this.$element);this.ulErrorManagement();this.bindHtml5Constraints();this.addConstraints();this.hasConstraints()&&this.bindValidationEvents()},setParent:function(a){this.$parent=d(a)},getParent:function(){return this.$parent},bindHtml5Constraints:function(){if(this.$element.hasClass("required")||this.$element.prop("required"))this.options.required= +!0;"undefined"!==typeof this.$element.attr("type")&&RegExp(this.$element.attr("type"),"i").test("email url number range")&&(this.options.type=this.$element.attr("type"),RegExp(this.options.type,"i").test("number range")&&(this.options.type="number","undefined"!==typeof this.$element.attr("min")&&this.$element.attr("min").length&&(this.options.min=this.$element.attr("min")),"undefined"!==typeof this.$element.attr("max")&&this.$element.attr("max").length&&(this.options.max=this.$element.attr("max")))); +"string"===typeof this.$element.attr("pattern")&&this.$element.attr("pattern").length&&(this.options.regexp=this.$element.attr("pattern"))},addConstraints:function(){for(var a in this.options){var b={};b[a]=this.options[a];this.addConstraint(b,!0)}},addConstraint:function(a,b){for(var c in a)c=c.toLowerCase(),"function"===typeof this.Validator.validators[c]&&(this.constraints[c]={name:c,requirements:a[c],valid:null},"required"===c&&(this.isRequired=!0),this.addCustomConstraintMessage(c));"undefined"=== +typeof b&&this.bindValidationEvents()},updateConstraint:function(a,b){for(var c in a)this.updtConstraint({name:c,requirements:a[c],valid:null},b)},updtConstraint:function(a,b){this.constraints[a.name]=d.extend(!0,this.constraints[a.name],a);"string"===typeof b&&(this.Validator.messages[a.name]=b);this.bindValidationEvents()},removeConstraint:function(a){a=a.toLowerCase();delete this.constraints[a];"required"===a&&(this.isRequired=!1);this.hasConstraints()?this.bindValidationEvents():"ParsleyForm"=== +typeof this.getParent()?this.getParent().removeItem(this.$element):this.destroy()},addCustomConstraintMessage:function(a){var b=a+("type"===a&&"undefined"!==typeof this.options[a]?this.options[a].charAt(0).toUpperCase()+this.options[a].substr(1):"")+"Message";"undefined"!==typeof this.options[b]&&this.Validator.addMessage("type"===a?this.options[a]:a,this.options[b],"type"===a)},bindValidationEvents:function(){this.valid=null;this.$element.addClass("parsley-validated");this.$element.off("."+this.type); +this.options.remote&&!/change/i.test(this.options.trigger)&&(this.options.trigger=!this.options.trigger?"change":" change");var a=(!this.options.trigger?"":this.options.trigger)+(/key/i.test(this.options.trigger)?"":" keyup");this.$element.is("select")&&(a+=/change/i.test(a)?"":" change");a=a.replace(/^\s+/g,"").replace(/\s+$/g,"");this.$element.on((a+" ").split(" ").join("."+this.type+" "),!1,d.proxy(this.eventValidation,this))},generateHash:function(){return"parsley-"+(Math.random()+"").substring(2)}, +getHash:function(){return this.hash},getVal:function(){return this.$element.data("value")||this.$element.val()},eventValidation:function(a){var b=this.getVal();if("keyup"===a.type&&!/keyup/i.test(this.options.trigger)&&!this.validatedOnce||"change"===a.type&&!/change/i.test(this.options.trigger)&&!this.validatedOnce||!this.isRadioOrCheckbox&&this.getLength(b)",errorElem:"
  • "},listeners:{onFieldValidate:function(){return!1},onFormSubmit:function(){},onFieldError:function(){},onFieldSuccess:function(){}}}; +d(window).on("load",function(){d('[data-validate="parsley"]').each(function(){d(this).parsley()})})}(window.jQuery||window.Zepto); diff --git a/wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore b/wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore new file mode 100644 index 00000000..af1b4bc3 --- /dev/null +++ b/wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore @@ -0,0 +1,2 @@ +*~ +*.js -- cgit v1.2.3 From db38728a7a59347847fb2fc5823a14f0f059f278 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 5 Sep 2013 18:36:15 -0500 Subject: Missed some files in last checkin so adding them now --- .../static/new/js_external/zxcvbn/.gitignore | 2 - wqflask/wqflask/templates/base.html | 12 +- .../wqflask/templates/new_security/_scripts.html | 4 +- .../templates/new_security/register_user.html | 158 +++++++++++++-------- wqflask/wqflask/views.py | 16 ++- 5 files changed, 118 insertions(+), 74 deletions(-) delete mode 100644 wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore diff --git a/wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore b/wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore deleted file mode 100644 index af1b4bc3..00000000 --- a/wqflask/wqflask/static/new/js_external/zxcvbn/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*~ -*.js diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html index cbed5859..7f72ff22 100644 --- a/wqflask/wqflask/templates/base.html +++ b/wqflask/wqflask/templates/base.html @@ -19,6 +19,7 @@ + {% block css %} {% endblock %} @@ -70,9 +71,9 @@
  • {% if g.identity.name=="anon" %} - Sign in + Sign in {% else %} - Sign out + Sign out {% endif %}
  • @@ -180,12 +181,13 @@ - + + + + {% block js %} {% endblock %} - - diff --git a/wqflask/wqflask/templates/new_security/_scripts.html b/wqflask/wqflask/templates/new_security/_scripts.html index 5a453dca..5fefe305 100644 --- a/wqflask/wqflask/templates/new_security/_scripts.html +++ b/wqflask/wqflask/templates/new_security/_scripts.html @@ -1,3 +1 @@ - - + diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html index d66365f4..d203d65f 100644 --- a/wqflask/wqflask/templates/new_security/register_user.html +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -1,66 +1,104 @@ -
    -

    * Already have an account?

    +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} +
    +
    +

    Register

    +

    + It's easy and fast to make an account. +

    +
    +
    -
    - Sign in using existing account -
    - -
    - -

    Don't have an account?

    - -
    Register here
    - -
    -
    - - -
    - -
    - -
    -
    +
    + + +
    +

    Already have an account?

    + -
    - -
    - -
    -
    - -
    - -
    - -
    -
    + Sign in using existing account -
    - -
    - -
    -
    - -
    - -
    - -
    -
    + +
    + +

    Don't have an account?

    + +
    Register here
    + + +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + + + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + +
    +
    -
    -
    - -
    -
    +{% endblock %} -
    +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} - -
    -{% include "security/_scripts.html" %} - diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 2b16dd77..6f14ac8d 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -51,10 +51,10 @@ from wqflask import user_manager def connect_db(): g.db = sqlalchemy.create_engine(app.config['DB_URI']) -@app.before_request -def trace_it(): - from wqflask import tracer - tracer.turn_on() +#@app.before_request +#def trace_it(): +# from wqflask import tracer +# tracer.turn_on() @app.route("/") def index_page(): @@ -292,6 +292,14 @@ def manage_groups(): return render_template("admin/group_manager.html", **template_vars.__dict__) +@app.route("/n/register") +def new_register(): + return render_template("new_security/register_user.html") + +@app.route("/n/login") +def new_login(): + return render_template("new_security/login_user.html") + def json_default_handler(obj): '''Based on http://stackoverflow.com/a/2680060/1175849''' # Handle datestamps -- cgit v1.2.3 From e91c058cd718fea25e38181098b1d0eb2ccf64cd Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 6 Sep 2013 17:33:19 -0500 Subject: Registration form validaiton without js works --- wqflask/secure_server.py | 2 +- wqflask/wqflask/model.py | 8 +++--- .../static/new/js_external/zxcvbn/zxcvbn-async.js | 1 + .../templates/new_security/register_user.html | 25 ++++++++++------ wqflask/wqflask/user_manager.py | 33 ++++++++++++++++++++++ wqflask/wqflask/views.py | 30 ++++++++++++++++++-- 6 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index 697ebfe0..df195bd2 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -36,4 +36,4 @@ if __name__ == '__main__': port=app.config['SERVER_PORT'], use_debugger=False, threaded=True, - use_reloader=False) + use_reloader=True) diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index c1b8d060..b3dfe746 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -47,11 +47,11 @@ class Role(db.Model, RoleMixin): class User(db.Model, UserMixin): id = db.Column(db.Integer(), primary_key=True) - email = db.Column(db.String(255), unique=True) - password = db.Column(db.String(255)) + email = db.Column(db.String(50), unique=True) + password = db.Column(db.String(50)) - name = db.Column(db.Unicode(255)) - organization = db.Column(db.Unicode(255)) + full_name = db.Column(db.Unicode(50)) + organization = db.Column(db.Unicode(50)) active = db.Column(db.Boolean()) confirmed_at = db.Column(db.DateTime()) diff --git a/wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js b/wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js new file mode 100644 index 00000000..404944d3 --- /dev/null +++ b/wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js @@ -0,0 +1 @@ +(function(){var a;a=function(){var a,b;b=document.createElement("script");b.src="//dl.dropbox.com/u/209/zxcvbn/zxcvbn.js";b.type="text/javascript";b.async=!0;a=document.getElementsByTagName("script")[0];return a.parentNode.insertBefore(b,a)};null!=window.attachEvent?window.attachEvent("onload",a):window.addEventListener("load",a,!1)}).call(this); diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html index d203d65f..6a7f4c9c 100644 --- a/wqflask/wqflask/templates/new_security/register_user.html +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -28,15 +28,26 @@

    Don't have an account?

    Register here
    - -
    + Please note: +
      + {% for error in errors %} +
    • {{error}}
    • + {% endfor %} +
    +
    + {% endif %} + +
    -
    @@ -44,7 +55,7 @@
    -
    @@ -52,7 +63,7 @@
    - +
    @@ -62,8 +73,7 @@
    - - + -
    diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 9e666bbd..d03910ca 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -9,6 +9,8 @@ from __future__ import print_function, division, absolute_import from wqflask import model +from utility import Bunch + from flask import Flask, g from pprint import pformat as pf @@ -43,6 +45,37 @@ class UserManager(object): print(" ID:", dataset.id) print(" Confidential:", dataset.check_confidentiality()) #print(" ---> self.datasets:", self.datasets) + + +class RegisterUser(object): + def __init__(self, kw): + self.errors = [] + user = Bunch() + + user.email_address = kw.get('email_address', '').strip() + if not (5 <= len(user.email_address) <= 50): + self.errors.append('Email Address needs to be between 5 and 50 characters.') + + user.full_name = kw.get('full_name', '').strip() + if not (5 <= len(user.full_name) <= 50): + self.errors.append('Full Name needs to be between 5 and 50 characters.') + + user.organization = kw.get('organization', '').strip() + if user.organization and not (5 <= len(user.organization) <= 50): + self.errors.append('Organization needs to be empty or between 5 and 50 characters.') + + user.password = kw.get('password', '') + if not (6 <= len(user.password) <= 30): + self.errors.append('Password needs to be between 6 and 30 characters.') + + if kw.get('password_confirm') != user.password: + self.errors.append("Passwords don't match.") + + if self.errors: + return + + + class GroupsManager(object): diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 6f14ac8d..9a0401d6 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -23,7 +23,8 @@ import sqlalchemy from wqflask import app -from flask import render_template, request, make_response, Response, Flask, g, config, jsonify +from flask import (render_template, request, make_response, Response, + Flask, g, config, jsonify, redirect, url_for) from wqflask import search_results from base.data_set import DataSet # Used by YAML in marker_regression @@ -292,9 +293,32 @@ def manage_groups(): return render_template("admin/group_manager.html", **template_vars.__dict__) -@app.route("/n/register") +@app.route("/n/register", methods=('GET', 'POST')) def new_register(): - return render_template("new_security/register_user.html") + params = None + errors = None + if request.form: + params = request.form + else: + params = request.args + if params: + result = user_manager.RegisterUser(params) + errors = result.errors + return render_template("new_security/register_user.html", values=params, errors=errors) + +#@app.route("/n/register_submit", methods=('POST',)) +#def register_submit(): +# print("request.args are: ", request.args) +# result = user_manager.RegisterUser(request.form) +# if result.errors: +# print("Redirecting") +# # 307 preserves the post on the redirect (maybe) +# errors = result.errors +# #errors = json.dumps(errors) +# print("request.args are: ", request.args) +# return render_template("new_security/register_user.html", errors=errors, values=request.form) +# #return redirect(url_for('new_register', errors=errors), code=307) + @app.route("/n/login") def new_login(): -- cgit v1.2.3 From 63c691fb11a33ac70c831f5651d284f38dc27b5b Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 24 Sep 2013 16:18:45 -0500 Subject: before deleting more of the security flask stuff --- wqflask/wqflask/templates/new_security/register_user.html | 2 +- wqflask/wqflask/user_manager.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html index 6a7f4c9c..755d0438 100644 --- a/wqflask/wqflask/templates/new_security/register_user.html +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -71,7 +71,7 @@
    + data-trigger="change" data-required="true" data-minlength="6">
    diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index d03910ca..c05bb9ce 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -65,8 +65,8 @@ class RegisterUser(object): self.errors.append('Organization needs to be empty or between 5 and 50 characters.') user.password = kw.get('password', '') - if not (6 <= len(user.password) <= 30): - self.errors.append('Password needs to be between 6 and 30 characters.') + if not (6 <= len(user.password)): + self.errors.append('Password needs to be at least 6 characters.') if kw.get('password_confirm') != user.password: self.errors.append("Passwords don't match.") @@ -87,3 +87,13 @@ class RolesManager(object): def __init__(self): self.roles = model.Role.query.all() print("Roles are:", self.roles) + + +#class Password(object): +# """To generate a master password: dd if=/dev/urandom bs=32 count=1 > master_salt""" +# +# master_salt = + + + + -- cgit v1.2.3 From faaa4f0a7b08d5a07c2a5a403774942e374ce0a3 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 27 Sep 2013 00:24:38 -0500 Subject: Before fixing an issue in pbkdf2 --- wqflask/wqflask/database.py | 22 ++++ wqflask/wqflask/model.py | 55 +++++---- wqflask/wqflask/pbkdf2.py | 130 +++++++++++++++++++++ wqflask/wqflask/templates/base.html | 2 +- .../templates/new_security/register_user.html | 6 +- wqflask/wqflask/user_manager.py | 76 ++++++++++-- wqflask/wqflask/views.py | 1 + 7 files changed, 251 insertions(+), 41 deletions(-) create mode 100644 wqflask/wqflask/database.py create mode 100644 wqflask/wqflask/pbkdf2.py diff --git a/wqflask/wqflask/database.py b/wqflask/wqflask/database.py new file mode 100644 index 00000000..d93987bc --- /dev/null +++ b/wqflask/wqflask/database.py @@ -0,0 +1,22 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +from wqflask import app + +#engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) +engine = create_engine(app.config['DB_URI'], convert_unicode=True) + +db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) +Base = declarative_base() +Base.query = db_session.query_property() + +def init_db(): + # import all modules here that might define models so that + # they will be registered properly on the metadata. Otherwise + # you will have to import them first before calling init_db() + #import yourapplication.models + import wqflask.model + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index b3dfe746..603cfbc4 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -1,10 +1,12 @@ from __future__ import print_function, division, absolute_import +import uuid + from flask.ext.sqlalchemy import SQLAlchemy -from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin +#from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin -from flask_security.forms import TextField -from flask_security.forms import RegisterForm +#from flask_security.forms import TextField +#from flask_security.forms import RegisterForm from wqflask import app @@ -37,18 +39,21 @@ db = SQLAlchemy(app) # Define models roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), - db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) - -class Role(db.Model, RoleMixin): - id = db.Column(db.Integer(), primary_key=True) - name = db.Column(db.String(80), unique=True) - description = db.Column(db.String(255)) - -class User(db.Model, UserMixin): - id = db.Column(db.Integer(), primary_key=True) - email = db.Column(db.String(50), unique=True) - password = db.Column(db.String(50)) + db.Column('user_id', db.Integer(), db.ForeignKey('user.the_id')), + db.Column('role_id', db.Integer(), db.ForeignKey('role.the_id'))) + +class Role(db.Model): + the_id = db.Column(db.Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + name = db.Column(db.Unicode(80), unique=True, nullable=False) + description = db.Column(db.Unicode(255)) + +class User(db.Model): + the_id = db.Column(db.Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + email_address = db.Column(db.Unicode(50), unique=True, nullable=False) + + password = db.Column(db.Unicode(24), nullable=False) + salt = db.Column(db.Unicode(32), nullable=False) + password_info = db.Column(db.Unicode(50)) full_name = db.Column(db.Unicode(50)) organization = db.Column(db.Unicode(50)) @@ -58,23 +63,23 @@ class User(db.Model, UserMixin): last_login_at = db.Column(db.DateTime()) current_login_at = db.Column(db.DateTime()) - last_login_ip = db.Column(db.String(39)) - current_login_ip = db.Column(db.String(39)) + last_login_ip = db.Column(db.Unicode(39)) + current_login_ip = db.Column(db.Unicode(39)) login_count = db.Column(db.Integer()) roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic')) # Setup Flask-Security -user_datastore = SQLAlchemyUserDatastore(db, User, Role) - -class ExtendedRegisterForm(RegisterForm): - name = TextField('name') - #print("name is:", name['_name'], vars(name)) - organization = TextField('organization') +#user_datastore = SQLAlchemyUserDatastore(db, User, Role) -security = Security(app, user_datastore, register_form=ExtendedRegisterForm) +#class ExtendedRegisterForm(RegisterForm): +# name = TextField('name') +# #print("name is:", name['_name'], vars(name)) +# organization = TextField('organization') +# +#security = Security(app, user_datastore, register_form=ExtendedRegisterForm) db.metadata.create_all(db.engine) -user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") +#user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") diff --git a/wqflask/wqflask/pbkdf2.py b/wqflask/wqflask/pbkdf2.py new file mode 100644 index 00000000..b7a7dd42 --- /dev/null +++ b/wqflask/wqflask/pbkdf2.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +""" + pbkdf2 + ~~~~~~ + + This module implements pbkdf2 for Python. It also has some basic + tests that ensure that it works. The implementation is straightforward + and uses stdlib only stuff and can be easily be copy/pasted into + your favourite application. + + Use this as replacement for bcrypt that does not need a c implementation + of a modified blowfish crypto algo. + + Example usage: + + >>> pbkdf2_hex('what i want to hash', 'the random salt') + 'fa7cc8a2b0a932f8e6ea42f9787e9d36e592e0c222ada6a9' + + How to use this: + + 1. Use a constant time string compare function to compare the stored hash + with the one you're generating:: + + def safe_str_cmp(a, b): + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + + 2. Use `os.urandom` to generate a proper salt of at least 8 byte. + Use a unique salt per hashed password. + + 3. Store ``algorithm$salt:costfactor$hash`` in the database so that + you can upgrade later easily to a different algorithm if you need + one. For instance ``PBKDF2-256$thesalt:10000$deadbeef...``. + + + :copyright: (c) Copyright 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import hmac +import hashlib +from struct import Struct +from operator import xor +from itertools import izip, starmap + + +_pack_int = Struct('>I').pack + + +def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Like :func:`pbkdf2_bin` but returns a hex encoded string.""" + return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex') + + +def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Returns a binary digest for the PBKDF2 hash algorithm of `data` + with the given `salt`. It iterates `iterations` time and produces a + key of `keylen` bytes. By default SHA-1 is used as hash function, + a different hashlib `hashfunc` can be provided. + """ + hashfunc = hashfunc or hashlib.sha1 + mac = hmac.new(data, None, hashfunc) + def _pseudorandom(x, mac=mac): + h = mac.copy() + h.update(x) + return map(ord, h.digest()) + buf = [] + for block in xrange(1, -(-keylen // mac.digest_size) + 1): + rv = u = _pseudorandom(salt + _pack_int(block)) + for i in xrange(iterations - 1): + u = _pseudorandom(''.join(map(chr, u))) + rv = starmap(xor, izip(rv, u)) + buf.extend(rv) + return ''.join(map(chr, buf))[:keylen] + + +def test(): + failed = [] + def check(data, salt, iterations, keylen, expected): + rv = pbkdf2_hex(data, salt, iterations, keylen) + if rv != expected: + print 'Test failed:' + print ' Expected: %s' % expected + print ' Got: %s' % rv + print ' Parameters:' + print ' data=%s' % data + print ' salt=%s' % salt + print ' iterations=%d' % iterations + print + failed.append(1) + + # From RFC 6070 + check('password', 'salt', 1, 20, + '0c60c80f961f0e71f3a9b524af6012062fe037a6') + check('password', 'salt', 2, 20, + 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957') + check('password', 'salt', 4096, 20, + '4b007901b765489abead49d926f721d065a429c1') + check('passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038') + check('pass\x00word', 'sa\x00lt', 4096, 16, + '56fa6aa75548099dcc37d7f03425e0c3') + # This one is from the RFC but it just takes for ages + ##check('password', 'salt', 16777216, 20, + ## 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984') + + # From Crypt-PBKDF2 + check('password', 'ATHENA.MIT.EDUraeburn', 1, 16, + 'cdedb5281bb2f801565a1122b2563515') + check('password', 'ATHENA.MIT.EDUraeburn', 1, 32, + 'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837') + check('password', 'ATHENA.MIT.EDUraeburn', 2, 16, + '01dbee7f4a9e243e988b62c73cda935d') + check('password', 'ATHENA.MIT.EDUraeburn', 2, 32, + '01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86') + check('password', 'ATHENA.MIT.EDUraeburn', 1200, 32, + '5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13') + check('X' * 64, 'pass phrase equals block size', 1200, 32, + '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1') + check('X' * 65, 'pass phrase exceeds block size', 1200, 32, + '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a') + + raise SystemExit(bool(failed)) + + +if __name__ == '__main__': + test() diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html index 7f72ff22..6e7119fe 100644 --- a/wqflask/wqflask/templates/base.html +++ b/wqflask/wqflask/templates/base.html @@ -70,7 +70,7 @@ Links
  • - {% if g.identity.name=="anon" %} + {% if "g.identity.name"=="anon" %} Sign in {% else %} Sign out diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html index 755d0438..2a02e7ca 100644 --- a/wqflask/wqflask/templates/new_security/register_user.html +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -45,7 +45,7 @@
    - +
    @@ -53,7 +53,7 @@
    - +
    @@ -61,7 +61,7 @@
    - +
    diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index c05bb9ce..3f5eee08 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -7,6 +7,17 @@ from __future__ import print_function, division, absolute_import """ +import os +import hashlib +import datetime +import time + +import simplejson as json + +import pbkdf2 + +from wqflask.database import db_session + from wqflask import model from utility import Bunch @@ -50,32 +61,73 @@ class UserManager(object): class RegisterUser(object): def __init__(self, kw): self.errors = [] - user = Bunch() + self.user = Bunch() - user.email_address = kw.get('email_address', '').strip() - if not (5 <= len(user.email_address) <= 50): + self.user.email_address = kw.get('email_address', '').strip() + if not (5 <= len(self.user.email_address) <= 50): self.errors.append('Email Address needs to be between 5 and 50 characters.') - user.full_name = kw.get('full_name', '').strip() - if not (5 <= len(user.full_name) <= 50): + self.user.full_name = kw.get('full_name', '').strip() + if not (5 <= len(self.user.full_name) <= 50): self.errors.append('Full Name needs to be between 5 and 50 characters.') - user.organization = kw.get('organization', '').strip() - if user.organization and not (5 <= len(user.organization) <= 50): + self.user.organization = kw.get('organization', '').strip() + if self.user.organization and not (5 <= len(self.user.organization) <= 50): self.errors.append('Organization needs to be empty or between 5 and 50 characters.') - user.password = kw.get('password', '') - if not (6 <= len(user.password)): + password = str(kw.get('password', '')) + if not (6 <= len(password)): self.errors.append('Password needs to be at least 6 characters.') - if kw.get('password_confirm') != user.password: + if kw.get('password_confirm') != password: self.errors.append("Passwords don't match.") if self.errors: - return + return + + print("No errors!") + + self.set_password(password) + + new_user = model.User(**self.user.__dict__) + db_session.add(new_user) + db_session.commit() - + def set_password(self, password): + pwfields = Bunch() + algo_string = "sha256" + algorithm = getattr(hashlib, algo_string) + pwfields.algorithm = "pbkdf2-" + algo_string + pwfields.salt = os.urandom(32) + + # lastpass did 100000 iterations in 2011 + pwfields.iterations = 100000 + pwfields.keylength = 24 + + pwfields.created = datetime.datetime.utcnow() + # One more check on password length + assert len(password) >= 6, "Password shouldn't be so short here" + + print("pwfields:", vars(pwfields)) + print("locals:", locals()) + start_time = time.time() + pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, algorithm) + print("Creating password took:", time.time() - start_time) + self.user.password = json.dumps(pwfields.__dict__, sort_keys=True) + + +#def combined_salt(user_salt): +# """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the +# salts aren't immediately known""" +# secret_salt = app.confing['SECRET_SALT'] +# assert len(user_salt) == 32 +# assert len(secret_salt) == 32 +# combined = "" +# for x, y in user_salt, secret_salt: +# combined = combined + x + y +# return combined + class GroupsManager(object): diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 9a0401d6..82d28eab 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -302,6 +302,7 @@ def new_register(): else: params = request.args if params: + print("Attempting to register the user...") result = user_manager.RegisterUser(params) errors = result.errors return render_template("new_security/register_user.html", values=params, errors=errors) -- cgit v1.2.3 From a5cd9ac66277b0eb87cad874e21967ae928797b1 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 27 Sep 2013 01:48:06 -0500 Subject: User registration beginning to work (at least the first stage) --- wqflask/wqflask/database.py | 5 ++- wqflask/wqflask/model.py | 68 ++++++++++++++++++++++------------------- wqflask/wqflask/pbkdf2.py | 12 +++++++- wqflask/wqflask/user_manager.py | 15 ++++++--- 4 files changed, 62 insertions(+), 38 deletions(-) diff --git a/wqflask/wqflask/database.py b/wqflask/wqflask/database.py index d93987bc..65ca7d0c 100644 --- a/wqflask/wqflask/database.py +++ b/wqflask/wqflask/database.py @@ -19,4 +19,7 @@ def init_db(): # you will have to import them first before calling init_db() #import yourapplication.models import wqflask.model - Base.metadata.create_all(bind=engine) \ No newline at end of file + Base.metadata.create_all(bind=engine) + + +init_db() \ No newline at end of file diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 603cfbc4..1f7297a2 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -10,8 +10,14 @@ from flask.ext.sqlalchemy import SQLAlchemy from wqflask import app +from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime +from sqlalchemy.orm import relationship, backref + +from wqflask.database import Base + # Create database connection object -db = SQLAlchemy(app) +#db = SQLAlchemy(app) + # Is this right? -Sam #from sqlalchemy.ext.declarative import declarative_base @@ -28,47 +34,48 @@ db = SQLAlchemy(app) # """ # print("in get cls is:", cls) # print(" key is {} : {}".format(type(key), key)) -# query = db.Model.query(cls) +# query = Model.query(cls) # print("query is: ", query) # record = query.get(key) # return record # # -#print("db.Model is:", vars(db.Model)) -#db.Model.get = get +#print("Model is:", vars(Model)) +#Model.get = get # Define models -roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('user.the_id')), - db.Column('role_id', db.Integer(), db.ForeignKey('role.the_id'))) - -class Role(db.Model): - the_id = db.Column(db.Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) - name = db.Column(db.Unicode(80), unique=True, nullable=False) - description = db.Column(db.Unicode(255)) - -class User(db.Model): - the_id = db.Column(db.Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) - email_address = db.Column(db.Unicode(50), unique=True, nullable=False) +#roles_users = Table('roles_users', +# Column('user_id', Integer(), ForeignKey('user.the_id')), +# Column('role_id', Integer(), ForeignKey('role.the_id'))) + +class Role(Base): + __tablename__ = "role" + the_id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + name = Column(Unicode(80), unique=True, nullable=False) + description = Column(Unicode(255)) + +class User(Base): + __tablename__ = "user" + the_id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + email_address = Column(Unicode(50), unique=True, nullable=False) - password = db.Column(db.Unicode(24), nullable=False) - salt = db.Column(db.Unicode(32), nullable=False) - password_info = db.Column(db.Unicode(50)) + # Todo: Turn on strict mode for Mysql + password = Column(Unicode(500), nullable=False) - full_name = db.Column(db.Unicode(50)) - organization = db.Column(db.Unicode(50)) + full_name = Column(Unicode(50)) + organization = Column(Unicode(50)) - active = db.Column(db.Boolean()) - confirmed_at = db.Column(db.DateTime()) + active = Column(Boolean()) + confirmed_at = Column(DateTime()) - last_login_at = db.Column(db.DateTime()) - current_login_at = db.Column(db.DateTime()) - last_login_ip = db.Column(db.Unicode(39)) - current_login_ip = db.Column(db.Unicode(39)) - login_count = db.Column(db.Integer()) + last_login_at = Column(DateTime()) + current_login_at = Column(DateTime()) + last_login_ip = Column(Unicode(39)) + current_login_ip = Column(Unicode(39)) + login_count = Column(Integer()) - roles = db.relationship('Role', secondary=roles_users, - backref=db.backref('users', lazy='dynamic')) + #roles = relationship('Role', secondary=roles_users, + # backref=backref('users', lazy='dynamic')) # Setup Flask-Security #user_datastore = SQLAlchemyUserDatastore(db, User, Role) @@ -80,6 +87,5 @@ class User(db.Model): # #security = Security(app, user_datastore, register_form=ExtendedRegisterForm) -db.metadata.create_all(db.engine) #user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") diff --git a/wqflask/wqflask/pbkdf2.py b/wqflask/wqflask/pbkdf2.py index b7a7dd42..f7f61a09 100644 --- a/wqflask/wqflask/pbkdf2.py +++ b/wqflask/wqflask/pbkdf2.py @@ -72,11 +72,21 @@ def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None): rv = u = _pseudorandom(salt + _pack_int(block)) for i in xrange(iterations - 1): u = _pseudorandom(''.join(map(chr, u))) - rv = starmap(xor, izip(rv, u)) + rv = list(starmap(xor, izip(rv, u))) buf.extend(rv) return ''.join(map(chr, buf))[:keylen] +def safe_str_cmp(a, b): + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + + + def test(): failed = [] def check(data, salt, iterations, keylen, expected): diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 3f5eee08..bafc1c3e 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -14,7 +14,7 @@ import time import simplejson as json -import pbkdf2 +from wqflask import pbkdf2 from wqflask.database import db_session @@ -100,12 +100,12 @@ class RegisterUser(object): algorithm = getattr(hashlib, algo_string) pwfields.algorithm = "pbkdf2-" + algo_string pwfields.salt = os.urandom(32) - - # lastpass did 100000 iterations in 2011 + + # https://forums.lastpass.com/viewtopic.php?t=84104 pwfields.iterations = 100000 pwfields.keylength = 24 - pwfields.created = datetime.datetime.utcnow() + pwfields.created_ts = datetime.datetime.utcnow().isoformat() # One more check on password length assert len(password) >= 6, "Password shouldn't be so short here" @@ -114,7 +114,12 @@ class RegisterUser(object): start_time = time.time() pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, algorithm) print("Creating password took:", time.time() - start_time) - self.user.password = json.dumps(pwfields.__dict__, sort_keys=True) + self.user.password = json.dumps(pwfields.__dict__, + sort_keys=True, + # See http://stackoverflow.com/a/12312896 + encoding="latin-1" + ) + #def combined_salt(user_salt): -- cgit v1.2.3 From 6c7fed36f892e92245cbfd9b89e4b8d6ff608e95 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Oct 2013 02:50:37 -0500 Subject: Half the verification code for email addresses is done --- wqflask/wqflask/model.py | 4 +- wqflask/wqflask/send_mail.py | 45 +++++++++ wqflask/wqflask/templates/email/verification.txt | 8 ++ wqflask/wqflask/user_manager.py | 123 +++++++++++++++++++++-- wqflask/wqflask/views.py | 5 + 5 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 wqflask/wqflask/send_mail.py create mode 100644 wqflask/wqflask/templates/email/verification.txt diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 1f7297a2..a3cd63a5 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -10,7 +10,7 @@ from flask.ext.sqlalchemy import SQLAlchemy from wqflask import app -from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime +from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime, Text from sqlalchemy.orm import relationship, backref from wqflask.database import Base @@ -60,7 +60,7 @@ class User(Base): email_address = Column(Unicode(50), unique=True, nullable=False) # Todo: Turn on strict mode for Mysql - password = Column(Unicode(500), nullable=False) + password = Column(Text, nullable=False) full_name = Column(Unicode(50)) organization = Column(Unicode(50)) diff --git a/wqflask/wqflask/send_mail.py b/wqflask/wqflask/send_mail.py new file mode 100644 index 00000000..be51ad0d --- /dev/null +++ b/wqflask/wqflask/send_mail.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import, division, print_function + +import datetime + +import simplejson as json + +from redis import StrictRedis +Redis = StrictRedis() + +import mailer + +def timestamp(): + ts = datetime.datetime.utcnow() + return ts.isoformat() + +def main(): + while True: + print("Waiting for message to show up in queue...") + msg = Redis.blpop("mail_queue") + + # Queue name is the first element, we want the second, which is the actual message + msg = msg[1] + + print("\nGot a msg in queue at {}: {}".format(timestamp(), msg)) + # Todo: Truncate mail_processed when it gets to long + Redis.rpush("mail_processed", msg) + process_message(msg) + + +def process_message(msg): + msg = json.loads(msg) + + message = mailer.Message() + message.From = msg['From'] + message.To = msg['To'] + message.Subject = msg['Subject'] + message.Body = msg['Body'] + + sender = mailer.Mailer('localhost') + sender.send(message) + print("Sent message at {}: {}\n".format(timestamp(), msg)) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/wqflask/wqflask/templates/email/verification.txt b/wqflask/wqflask/templates/email/verification.txt new file mode 100644 index 00000000..29229c68 --- /dev/null +++ b/wqflask/wqflask/templates/email/verification.txt @@ -0,0 +1,8 @@ +Thank you for signing up for GeneNetwork. + +We need to verify your email address. + +To do that please click the following link, or cut and paste it into your browser window: + +{{ url_for_hmac("verify", code = verification_code, _external=True )}} + diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index bafc1c3e..159a0ffc 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -12,8 +12,23 @@ import hashlib import datetime import time +import uuid +import hashlib +import hmac + import simplejson as json +from redis import StrictRedis +Redis = StrictRedis() + + +from flask import Flask, g, render_template, url_for + +from wqflask import app + + +from pprint import pformat as pf + from wqflask import pbkdf2 from wqflask.database import db_session @@ -22,9 +37,7 @@ from wqflask import model from utility import Bunch -from flask import Flask, g -from pprint import pformat as pf from base.data_set import create_datasets_list @@ -89,21 +102,25 @@ class RegisterUser(object): self.set_password(password) - new_user = model.User(**self.user.__dict__) - db_session.add(new_user) + self.new_user = model.User(**self.user.__dict__) + db_session.add(self.new_user) db_session.commit() + self.send_email_verification() + def set_password(self, password): pwfields = Bunch() - algo_string = "sha256" - algorithm = getattr(hashlib, algo_string) - pwfields.algorithm = "pbkdf2-" + algo_string + + pwfields.algorithm = "pbkdf2" + pwfields.hashfunc = "sha256" + hashfunc = getattr(hashlib, pwfields.hashfunc) + pwfields.salt = os.urandom(32) # https://forums.lastpass.com/viewtopic.php?t=84104 pwfields.iterations = 100000 - pwfields.keylength = 24 + pwfields.keylength = 32 pwfields.created_ts = datetime.datetime.utcnow().isoformat() # One more check on password length @@ -111,16 +128,102 @@ class RegisterUser(object): print("pwfields:", vars(pwfields)) print("locals:", locals()) + + # On our computer it takes around 1.4 seconds start_time = time.time() - pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, algorithm) - print("Creating password took:", time.time() - start_time) + pwfields.password = pbkdf2.pbkdf2_hex(password, pwfields.salt, pwfields.iterations, pwfields.keylength, hashfunc) + pwfields.encrypt_time = round(time.time() - start_time, 3) + + print("Creating password took:", pwfields.encrypt_time) + self.user.password = json.dumps(pwfields.__dict__, sort_keys=True, # See http://stackoverflow.com/a/12312896 encoding="latin-1" ) + def send_email_verification(self): + verification_code = str(uuid.uuid4()) + key = "verification_code:" + verification_code + + data = json.dumps(dict(the_id=self.new_user.the_id, + timestamp=datetime.datetime.utcnow().isoformat()) + ) + + Redis.set(key, data) + two_days = 60 * 60 * 24 * 2 + Redis.expire(key, two_days) + to = self.user.email_address + subject = "GeneNetwork email verification" + body = render_template("email/verification.txt", + verification_code = verification_code) + send_email(to, subject, body) + + +def verify_email(request): + print("in verify_email request.url is:", request.url) + verify_url_hmac(request.url) + verification_code = request.args['code'] + data = Redis.get("verification_code:" + verification_code) + data = json.loads(data) + print("data is:", data) + + + +################################# Sign and unsign ##################################### +def url_for_hmac(endpoint, **values): + """Like url_for but adds an hmac at the end to insure the url hasn't been tampered with""" + + url = url_for(endpoint, **values) + + hm = actual_hmac_creation(url) + if '?' in url: + combiner = "&" + else: + combiner = "?" + return url + combiner + "hm=" + hm + +def verify_url_hmac(url): + """Pass in a url that was created with url_hmac and this assures it hasn't been tampered with""" + print("url passed in to verify is:", url) + # Verify parts are correct at the end - we expect to see &hm= or ?hm= followed by an hmac + assert url[-23:-20] == "hm=", "Unexpected url (stage 1)" + assert url[-24] in ["?", "&"], "Unexpected url (stage 2)" + hmac = url[-20:] + url = url[:-24] # Url without any of the hmac stuff + + #print("before urlsplit, url is:", url) + #url = divide_up_url(url)[1] + #print("after urlsplit, url is:", url) + + hm = actual_hmac_creation(url) + + assert hm == hmac, "Unexpected url (stage 3)" + +def actual_hmac_creation(url): + """Helper function to create the actual hmac""" + + secret = app.config['SECRET_HMAC_CODE'] + + hmaced = hmac.new(secret, url, hashlib.sha1) + hm = hmaced.hexdigest() + # "Conventional wisdom is that you don't lose much in terms of security if you throw away up to half of the output." + # http://www.w3.org/QA/2009/07/hmac_truncation_in_xml_signatu.html + hm = hm[:20] + return hm + +app.jinja_env.globals.update(url_for_hmac=url_for_hmac) + +####################################################################################### + +def send_email(to, subject, body): + msg = json.dumps(dict(From="no-reply@genenetwork.org", + To=to, + Subject=subject, + Body=body)) + Redis.rpush("mail_queue", msg) + #def combined_salt(user_salt): # """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 82d28eab..b552e160 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -292,6 +292,11 @@ def manage_groups(): template_vars = user_manager.GroupsManager(request.args) return render_template("admin/group_manager.html", **template_vars.__dict__) +@app.route("/manage/verify") +def verify(): + user_manager.verify_email(request) + return "foo" + @app.route("/n/register", methods=('GET', 'POST')) def new_register(): -- cgit v1.2.3 From 91d05ab30a60671286b19e174ec0292dc6aa1af3 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 3 Oct 2013 01:21:19 -0500 Subject: Much more progess on logging in: * Verifying emails now works * Redid user manager --- wqflask/wqflask/database.py | 3 +- wqflask/wqflask/model.py | 32 +++++++++++++------ wqflask/wqflask/templates/admin/user_manager.html | 14 ++++----- .../wqflask/templates/new_security/thank_you.html | 32 +++++++++++++++++++ .../wqflask/templates/new_security/verified.html | 32 +++++++++++++++++++ wqflask/wqflask/user_manager.py | 36 ++++++++++++++++------ wqflask/wqflask/views.py | 23 +++++++++----- 7 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 wqflask/wqflask/templates/new_security/thank_you.html create mode 100644 wqflask/wqflask/templates/new_security/verified.html diff --git a/wqflask/wqflask/database.py b/wqflask/wqflask/database.py index 65ca7d0c..e55f06a7 100644 --- a/wqflask/wqflask/database.py +++ b/wqflask/wqflask/database.py @@ -19,7 +19,8 @@ def init_db(): # you will have to import them first before calling init_db() #import yourapplication.models import wqflask.model + print("Creating all..") Base.metadata.create_all(bind=engine) - + print("Done creating all...") init_db() \ No newline at end of file diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index a3cd63a5..8e7a823e 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -13,7 +13,7 @@ from wqflask import app from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime, Text from sqlalchemy.orm import relationship, backref -from wqflask.database import Base +from wqflask.database import Base, init_db # Create database connection object #db = SQLAlchemy(app) @@ -50,13 +50,13 @@ from wqflask.database import Base class Role(Base): __tablename__ = "role" - the_id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) name = Column(Unicode(80), unique=True, nullable=False) description = Column(Unicode(255)) class User(Base): __tablename__ = "user" - the_id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) email_address = Column(Unicode(50), unique=True, nullable=False) # Todo: Turn on strict mode for Mysql @@ -65,18 +65,28 @@ class User(Base): full_name = Column(Unicode(50)) organization = Column(Unicode(50)) - active = Column(Boolean()) - confirmed_at = Column(DateTime()) + active = Column(Boolean(), nullable=False, default=True) - last_login_at = Column(DateTime()) - current_login_at = Column(DateTime()) - last_login_ip = Column(Unicode(39)) - current_login_ip = Column(Unicode(39)) - login_count = Column(Integer()) + registration_info = Column(Text) # json detailing when they were registered, etc. + + confirmed = Column(Text) # json detailing when they confirmed, etc. + + #last_login_at = Column(DateTime()) + #current_login_at = Column(DateTime()) + #last_login_ip = Column(Unicode(39)) + #current_login_ip = Column(Unicode(39)) + #login_count = Column(Integer()) #roles = relationship('Role', secondary=roles_users, # backref=backref('users', lazy='dynamic')) +class Login(Base): + __tablename__ = "login" + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + user = Column(Unicode(36), ForeignKey('user.id')) + timestamp = Column(DateTime()) + ip_address = Column(Unicode(39)) + # Setup Flask-Security #user_datastore = SQLAlchemyUserDatastore(db, User, Role) @@ -89,3 +99,5 @@ class User(Base): #user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") + + diff --git a/wqflask/wqflask/templates/admin/user_manager.html b/wqflask/wqflask/templates/admin/user_manager.html index 14cd12e0..1308ff4b 100644 --- a/wqflask/wqflask/templates/admin/user_manager.html +++ b/wqflask/wqflask/templates/admin/user_manager.html @@ -16,20 +16,20 @@ - - + + {% for user in users %} - - - - + + + {% endfor %}
    ID EmailConfirmed atOrganization ActiveConfirmed
    - {{ user.id }} + + {{ user.email_address }} {{ user.email }}{{ user.confirmed_at }}{{ user.active }}{{ user.organization }}{{ 'Yes' if user.active else 'No' }}{{ 'True' if user.confirmed else 'False' }}
    diff --git a/wqflask/wqflask/templates/new_security/thank_you.html b/wqflask/wqflask/templates/new_security/thank_you.html new file mode 100644 index 00000000..5aa11ebf --- /dev/null +++ b/wqflask/wqflask/templates/new_security/thank_you.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} +
    +
    +

    Thank you

    +

    + Thanks for verifying. +

    +
    +
    + +
    + + +

    Enjoy using the site.

    + +

    Go to the homepage

    . +
    + +{% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} + diff --git a/wqflask/wqflask/templates/new_security/verified.html b/wqflask/wqflask/templates/new_security/verified.html new file mode 100644 index 00000000..97cb7807 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/verified.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} +
    +
    +

    Thank you

    +

    + Thanks for verifying. +

    +
    +
    + +
    + + +

    Enjoy using the site.

    + +

    Go to the homepage

    . +
    + +{% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} + diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 159a0ffc..b967c86f 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -22,7 +22,7 @@ from redis import StrictRedis Redis = StrictRedis() -from flask import Flask, g, render_template, url_for +from flask import Flask, g, render_template, url_for, request from wqflask import app @@ -41,8 +41,11 @@ from utility import Bunch from base.data_set import create_datasets_list -#from app import db -print("globals are:", globals()) + + +def timestamp(): + return datetime.datetime.utcnow().isoformat() + class UsersManager(object): @@ -54,7 +57,7 @@ class UsersManager(object): class UserManager(object): def __init__(self, kw): - self.user_id = int(kw['user_id']) + self.user_id = kw['user_id'] print("In UserManager locals are:", pf(locals())) #self.user = model.User.get(user_id) #print("user is:", user) @@ -73,6 +76,7 @@ class UserManager(object): class RegisterUser(object): def __init__(self, kw): + self.thank_you_mode = False self.errors = [] self.user = Bunch() @@ -102,12 +106,16 @@ class RegisterUser(object): self.set_password(password) + self.user.registration_info = json.dumps(basic_info(), sort_keys=True) + self.new_user = model.User(**self.user.__dict__) db_session.add(self.new_user) db_session.commit() self.send_email_verification() + self.thank_you_mode = True + def set_password(self, password): pwfields = Bunch() @@ -122,7 +130,7 @@ class RegisterUser(object): pwfields.iterations = 100000 pwfields.keylength = 32 - pwfields.created_ts = datetime.datetime.utcnow().isoformat() + pwfields.created_ts = timestamp() # One more check on password length assert len(password) >= 6, "Password shouldn't be so short here" @@ -146,8 +154,8 @@ class RegisterUser(object): verification_code = str(uuid.uuid4()) key = "verification_code:" + verification_code - data = json.dumps(dict(the_id=self.new_user.the_id, - timestamp=datetime.datetime.utcnow().isoformat()) + data = json.dumps(dict(id=self.new_user.id, + timestamp=timestamp()) ) Redis.set(key, data) @@ -158,15 +166,25 @@ class RegisterUser(object): body = render_template("email/verification.txt", verification_code = verification_code) send_email(to, subject, body) - + + +def basic_info(): + return dict(timestamp = timestamp(), + ip_address = request.remote_addr, + user_agent = request.headers.get('User-Agent')) -def verify_email(request): +def verify_email(): print("in verify_email request.url is:", request.url) verify_url_hmac(request.url) verification_code = request.args['code'] data = Redis.get("verification_code:" + verification_code) data = json.loads(data) print("data is:", data) + user = model.User.query.get(data['id']) + user.confirmed = json.dumps(basic_info(), sort_keys=True) + db_session.commit() + + diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index b552e160..fe91e014 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -267,15 +267,20 @@ def sharing_info_page(): template_vars = SharingInfoPage.SharingInfoPage(fd) return template_vars -# Take this out or secure it before going into production +# Take this out or secure it before g[umlfoing into production @app.route("/get_temp_data") def get_temp_data(): temp_uuid = request.args['key'] return flask.jsonify(temp_data.TempData(temp_uuid).get_all()) -@app.route("/thank_you") -def thank_you(): - return render_template("security/thank_you.html") +#@app.route("/thank_you") +#def thank_you(): +# return render_template("security/thank_you.html") + +@app.route("/manage/verify") +def verify(): + user_manager.verify_email() + return render_template("new_security/verified.html") @app.route("/manage/users") def manage_users(): @@ -292,10 +297,7 @@ def manage_groups(): template_vars = user_manager.GroupsManager(request.args) return render_template("admin/group_manager.html", **template_vars.__dict__) -@app.route("/manage/verify") -def verify(): - user_manager.verify_email(request) - return "foo" + @app.route("/n/register", methods=('GET', 'POST')) @@ -310,6 +312,11 @@ def new_register(): print("Attempting to register the user...") result = user_manager.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/thank_you.html") + return render_template("new_security/register_user.html", values=params, errors=errors) #@app.route("/n/register_submit", methods=('POST',)) -- cgit v1.2.3 From e13f0c576ca75af57abac83851d206e113dabaad Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 6 Oct 2013 22:10:44 -0500 Subject: Before writing login code --- .../wqflask/templates/new_security/registered.html | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 wqflask/wqflask/templates/new_security/registered.html diff --git a/wqflask/wqflask/templates/new_security/registered.html b/wqflask/wqflask/templates/new_security/registered.html new file mode 100644 index 00000000..391a6044 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/registered.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} +
    +
    +

    Thank you

    +

    + Thanks for verifying. +

    +
    +
    + +
    + + +

    You will receive an email with the subject "GeneNetwork email verification".

    + +

    You must click the link in the email to complete registration.

    + +

    If you don't see the email, check your spam folder.

    +
    + +{% endblock %} + +{% block js %} + + + {% include "new_security/_scripts.html" %} + + +{% endblock %} + -- cgit v1.2.3 From a55a1b941864a9574b8177349b8c9c750f379c72 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 7 Oct 2013 02:14:41 -0500 Subject: Worked on logins, session_ids, flash messages, etc. --- wqflask/secure_server.py | 7 +- wqflask/utility/__init__.py | 23 ++++ wqflask/wqflask/model.py | 12 +- wqflask/wqflask/templates/base.html | 29 +++++ wqflask/wqflask/templates/index_page.html | 7 +- .../wqflask/templates/new_security/login_user.html | 144 ++++++++++++--------- .../wqflask/templates/new_security/thank_you.html | 4 +- wqflask/wqflask/user_manager.py | 94 +++++++++++--- wqflask/wqflask/views.py | 43 +++--- 9 files changed, 260 insertions(+), 103 deletions(-) diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index df195bd2..a77abf7e 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -25,8 +25,11 @@ app.logger.addHandler(file_handler) import logging_tree logging_tree.printout() -import sys -print("At startup, path is:", sys.path) +#import sys +#print("At startup, path is:", sys.path) + +from werkzeug.contrib.fixers import ProxyFix +app.wsgi_app = ProxyFix(app.wsgi_app) #print("app.config is:", app.config) diff --git a/wqflask/utility/__init__.py b/wqflask/utility/__init__.py index d0e4a3fa..d9856eed 100755 --- a/wqflask/utility/__init__.py +++ b/wqflask/utility/__init__.py @@ -1,5 +1,6 @@ from pprint import pformat as pf +# Todo: Move these out of __init__ class Bunch(object): """Like a dictionary but using object notation""" @@ -10,3 +11,25 @@ class Bunch(object): return pf(self.__dict__) +class Struct(object): + '''The recursive class for building and representing objects with. + + From http://stackoverflow.com/a/6573827/1175849 + + ''' + + def __init__(self, obj): + for k, v in obj.iteritems(): + if isinstance(v, dict): + setattr(self, k, Struct(v)) + else: + setattr(self, k, v) + + def __getitem__(self, val): + return self.__dict__[val] + + def __repr__(self): + return '{%s}' % str(', '.join('%s : %s' % (k, repr(v)) for + (k, v) in self.__dict__.iteritems())) + + diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 8e7a823e..5c514bde 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -1,7 +1,9 @@ from __future__ import print_function, division, absolute_import import uuid +import datetime +from flask import request from flask.ext.sqlalchemy import SQLAlchemy #from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin @@ -84,9 +86,15 @@ class Login(Base): __tablename__ = "login" id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) user = Column(Unicode(36), ForeignKey('user.id')) - timestamp = Column(DateTime()) + timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) ip_address = Column(Unicode(39)) - + successful = Column(Boolean(), nullable=False) # False if wrong password was entered + session_id = Column(Text) # Set only if successfully logged in, otherwise should be blank + + def __init__(self, user): + self.user = user.id + self.ip_address = request.remote_addr + # Setup Flask-Security #user_datastore = SQLAlchemyUserDatastore(db, User, Role) diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html index 6e7119fe..077e4705 100644 --- a/wqflask/wqflask/templates/base.html +++ b/wqflask/wqflask/templates/base.html @@ -26,10 +26,39 @@ +{% macro header(main, second) %} +
    +
    +

    Login

    +

    + Gain access to GeneNetwork. +

    +
    +
    + + {{ flash_me() }} +{% endmacro %} + + +{% macro flash_me() -%} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    +
    + {% for category, message in messages %} +
    {{ message }}
    + {% endfor %} +
    + {% endif %} + {% endwith %} +{% endmacro %} + +
  • - {% if "g.identity.name"=="anon" %} - Sign in + {% if g.user_session.logged_in %} + Sign out {% else %} - Sign out + Sign in {% endif %}
  • diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 7c1761ba..a2dff7f2 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -23,7 +23,8 @@ from redis import StrictRedis Redis = StrictRedis() -from flask import Flask, g, render_template, url_for, request, make_response, redirect, flash +from flask import (Flask, g, render_template, url_for, request, make_response, + redirect, flash) from wqflask import app @@ -49,6 +50,37 @@ def timestamp(): + +class UserSession(object): + cookie_name = 'session_id' + + def __init__(self): + cookie = request.cookies.get(self.cookie_name) + if not cookie: + self.logged_in = False + return + else: + session_id, separator, session_id_signature = cookie.partition(':') + assert len(session_id) == 36, "Is session_id a uuid?" + assert separator == ":", "Expected a : here" + assert session_id_signature == actual_hmac_creation(session_id), "Uh-oh, someone tampering with the cookie?" + self.redis_key = "session_id:" + session_id + print("self.redis_key is:", self.redis_key) + self.session_id = session_id + self.record = Redis.hgetall(self.redis_key) + print("record is:", self.record) + self.logged_in = True + + + def delete_session(self): + # And more importantly delete the redis record + Redis.delete(self.cookie_name) + print("At end of delete_session") + +@app.before_request +def before_request(): + g.user_session = UserSession() + class UsersManager(object): def __init__(self): self.users = model.User.query.all() @@ -173,7 +205,6 @@ class RegisterUser(object): class Password(object): def __init__(self, unencrypted_password, salt, iterations, keylength, hashfunc): - print("in Password __init__ locals are:", locals()) hashfunc = getattr(hashlib, hashfunc) print("hashfunc is:", hashfunc) # On our computer it takes around 1.4 seconds in 2013 @@ -229,6 +260,7 @@ def login(): #session_id = "session_id:{}".format(login_rec.session_id) session_id_signature = actual_hmac_creation(login_rec.session_id) session_id_signed = login_rec.session_id + ":" + session_id_signature + print("session_id_signed:", session_id_signed) session = dict(login_time = time.time(), user_id = user.id, @@ -236,10 +268,12 @@ def login(): flash("Thank you for logging in.", "alert-success") - Redis.hmset("session_id:" + login_rec.session_id, session) + key = "session_id:" + login_rec.session_id + print("Key when signing:", key) + Redis.hmset(key, session) - response = make_response(redirect('/')) - response.set_cookie('session_id', session_id_signed) + response = make_response(redirect(url_for('index_page'))) + response.set_cookie(UserSession.cookie_name, session_id_signed) else: login_rec.successful = False flash("Invalid email-address or password. Please try again.", "alert-error") @@ -247,9 +281,16 @@ def login(): db_session.add(login_rec) db_session.commit() return response - - def logout(): - pass + +@app.route("/n/logout") +def logout(): + print("Logging out...") + UserSession().delete_session() + flash("You are now logged out. We hope you come back soon!") + response = make_response(redirect(url_for('index_page'))) + # Delete the cookie + response.set_cookie(UserSession.cookie_name, '', expires=0) + return response ################################# Sign and unsign ##################################### @@ -283,12 +324,12 @@ def verify_url_hmac(url): assert hm == hmac, "Unexpected url (stage 3)" -def actual_hmac_creation(url): +def actual_hmac_creation(stringy): """Helper function to create the actual hmac""" secret = app.config['SECRET_HMAC_CODE'] - hmaced = hmac.new(secret, url, hashlib.sha1) + hmaced = hmac.new(secret, stringy, hashlib.sha1) hm = hmaced.hexdigest() # "Conventional wisdom is that you don't lose much in terms of security if you throw away up to half of the output." # http://www.w3.org/QA/2009/07/hmac_truncation_in_xml_signatu.html -- cgit v1.2.3