aboutsummaryrefslogtreecommitdiff
path: root/wqflask
diff options
context:
space:
mode:
Diffstat (limited to 'wqflask')
-rw-r--r--wqflask/wqflask/database.py22
-rw-r--r--wqflask/wqflask/model.py55
-rw-r--r--wqflask/wqflask/pbkdf2.py130
-rw-r--r--wqflask/wqflask/templates/base.html2
-rw-r--r--wqflask/wqflask/templates/new_security/register_user.html6
-rw-r--r--wqflask/wqflask/user_manager.py76
-rw-r--r--wqflask/wqflask/views.py1
7 files changed, 251 insertions, 41 deletions
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 @@
<a href="/links">Links</a>
</li>
<li class="">
- {% if g.identity.name=="anon" %}
+ {% if "g.identity.name"=="anon" %}
<a id="login_out" class="modalize" href="/n/register">Sign in</a>
{% else %}
<a id="login_out" href="/n/logout">Sign out</a>
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 @@
<fieldset>
<div class="control-group">
- <label class="control-label" for="email">Email Address</label>
+ <label class="control-label" for="email_address">Email Address</label>
<div class="controls">
<input id="email_address" name="email_address" class="focused" type="text" value="{{values.email_address}}"
data-trigger="change" data-required="true" data-type="email" data-maxlength="50">
@@ -53,7 +53,7 @@
</div>
<div class="control-group">
- <label class="control-label" for="email">Full Name</label>
+ <label class="control-label" for="full_name">Full Name</label>
<div class="controls">
<input id="full_name" name="full_name" type="text" value="{{values.full_name}}"
data-trigger="change" data-required="true" data-minlength="5" data-maxlength="50">
@@ -61,7 +61,7 @@
</div>
<div class="control-group">
- <label class="control-label" for="email">Organization</label>
+ <label class="control-label" for="organization">Organization</label>
<div class="controls">
<input id="organization" name="organization" type="text" value="{{values.organization}}" data-minlength="3" data-maxlength="50">
</div>
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)