From fe54bd89d3ee21fdb1c0d1c3274a243349cafc1c Mon Sep 17 00:00:00 2001 From: Eric Urban Date: Sun, 11 Aug 2013 16:29:00 -0400 Subject: [PATCH] working on invites, doing some refactoring and testing as well --- auth.py | 39 +----------------- fairywren.sql | 10 +++++ html/index.html | 2 +- html/invite.html | 40 +++++++++++++++++++ html/js/fairywren.js | 13 +++++- html/js/invite.js | 64 ++++++++++++++++++++++++++++++ permissions.sql | 2 + test/testUsers.py | 15 +++++++ test/testWebApi.py | 19 +++++---- users.py | 94 +++++++++++++++++++++++++++++++++++++++++--- webapi.py | 8 +++- 11 files changed, 250 insertions(+), 56 deletions(-) create mode 100644 html/invite.html create mode 100644 html/js/invite.js diff --git a/auth.py b/auth.py index 6a51597..dc774cd 100644 --- a/auth.py +++ b/auth.py @@ -9,8 +9,7 @@ class Auth(object): - def __init__(self,salt): - self.salt = salt + def __init__(self): self.log = logging.getLogger('fairywren.auth') self.log.info('Created') @@ -48,43 +47,7 @@ def changePassword(self,userId,pwHash): return True - def _saltPwhash(self,pwHash): - storedHash = hashlib.sha512() - storedHash.update(self.salt) - storedHash.update(pwHash) - return base64.urlsafe_b64encode(storedHash.digest()).replace('=','') - def addUser(self,username,pwHash): - self.log.debug('Trying to add user %s',username) - secretKey = hashlib.sha512() - - randomValue = os.urandom(1024) - secretKey.update(randomValue) - - saltedPw = self._saltPwhash(pwHash) - - with self.connPool.item() as conn: - cur = conn.cursor() - - try: - cur.execute("INSERT into users (name,password,secretKey) VALUES(%s,%s,%s) returning users.id;", - (username, - saltedPw, - base64.urlsafe_b64encode(secretKey.digest()).replace('=',''),) ) - except IntegrityError as e: - self.log.error(e) - conn.rollback() - cur.close() - return None - - conn.commit() - - newId, = cur.fetchone() - cur.close() - conn.close() - self.log.debug('Added user, new id %.8x', newId) - return 'api/users/%.8x' % newId - def authenticateSecretKey(self,key): with self.connPool.item() as conn: diff --git a/fairywren.sql b/fairywren.sql index 68e47bc..5bad563 100644 --- a/fairywren.sql +++ b/fairywren.sql @@ -16,6 +16,16 @@ CREATE TABLE torrents( PRIMARY KEY(id,infoHash) ); +CREATE TABLE invites( + id SERIAL UNIQUE, + secret char(43) UNIQUE, + inviter INTEGER REFERENCES users(id) NOT NULL, + creationDate TIMESTAMP WITHOUT TIME ZONE NOT NULL, + invitee INTEGER REFERENCES users(id) NULL, + accepted TIMESTAMP WITHOUT TIME ZONE NULL, + PRIMARY KEY(id) +); + CREATE TABLE roles( id SERIAL UNIQUE, name varchar NOT NULL UNIQUE, diff --git a/html/index.html b/html/index.html index 01a5775..2242e83 100644 --- a/html/index.html +++ b/html/index.html @@ -27,7 +27,7 @@


- +
diff --git a/html/invite.html b/html/invite.html new file mode 100644 index 0000000..b2d642d --- /dev/null +++ b/html/invite.html @@ -0,0 +1,40 @@ + + + + + +Invite + + + + + + + + + + + + + +
+ +
+ +
+ +
+
Register
+
+
+
+
+ +
+
+ +
+ + + + diff --git a/html/js/fairywren.js b/html/js/fairywren.js index 9ea7aee..1a642bd 100644 --- a/html/js/fairywren.js +++ b/html/js/fairywren.js @@ -1,6 +1,7 @@ Fairywren = {}; Fairywren.MIN_PASSWORD_LENGTH = 12; +Fairywren.MIN_USERNAME_LENGTH = 4; Fairywren.serverErrorHandler = function(jqXhr,textStatus,errorThrown,element) { @@ -77,13 +78,21 @@ Fairywren.validateUsername = function(username) for(var i = 0 ; i < username.length;++i){ if ( -1 === ACCEPTED.indexOf(username[i])) { - rejected.push(username[i]); + if(rejected.indexOf(username[i])===-1) + { + rejected.push(username[i]); + } } } if(rejected.length !==0) { - return 'The following characters are not allowed in usernames: ' + rejected.join(''); + return 'The following characters are not allowed in usernames: "' + rejected.join('') + '"'; + } + + if(username.length < Fairywren.MIN_USERNAME_LENGTH) + { + return 'Username too short'; } return null; diff --git a/html/js/invite.js b/html/js/invite.js new file mode 100644 index 0000000..2a71610 --- /dev/null +++ b/html/js/invite.js @@ -0,0 +1,64 @@ +$(document).ready(function(){ + var hash = window.location.hash; + + var registerButton = $("input#register"); + + if(hash.length === 0) + { + //User got here on accident or something. Display error message + //and depart + } + + var inviteHref = hash.slice(1); + + //Retrieve the invite, check to see if it is valid + jQuery.get(inviteHref). + done( + function(data) + { + //Check to see if it has been claimed + + + } + ).fail(function(jqXhr,textStatus,errorThrown) + { + Fairywren.serverErrorHandler(jqXhr,textStatus,errorThrown,$("#message")); + + }); + + }); + +Fairywren.register = function() +{ + var errDisplay = $("#message"); + errDisplay.text(''); + var username = $("input#username"); + + var validUsername = Fairywren.validateUsername(username.val()); + + if(validUsername !== null) + { + errDisplay.text(validUsername); + return; + } + + var password0 = $("input#password0"); + var password1 = $("input#password1"); + + + var validPassword = Fairywren.validatePassword(password0.val()); + if( validPassword !== null) + { + errDisplay.text(validPassword); + return; + } + + if(password0.val() !== password1.val()) + { + errDisplay.text("Password does not match"); + return; + } + +} + + diff --git a/permissions.sql b/permissions.sql index 5f642c2..9949e94 100644 --- a/permissions.sql +++ b/permissions.sql @@ -5,6 +5,8 @@ GRANT UPdATE,SELECT on users_id_seq to webapi; GRANT ALL on roles to webapi; GRANT UPDATE,SELECT on roles_id_seq to webapi; GRANT ALL on rolemember to webapi; +GRANT ALL on invites to webapi; +GRANT UPDATE,SELECT on invites_id_seq to webapi; GRANT SELECT on torrents to tracker; GRANT SELECT on users to tracker; diff --git a/test/testUsers.py b/test/testUsers.py index 64633ed..e5249a8 100644 --- a/test/testUsers.py +++ b/test/testUsers.py @@ -11,7 +11,22 @@ import getpass import signal +from TestPostgres import TestPostgres +import users +class TestUsersEmptyDatabase(TestPostgres): + def setUp(self): + TestPostgres.setUp(self) + self.users = users.Users() + self.users.setConnectionPool(self.getConnectionPool()) + + def test_createInvite(self): + with self.assertRaisesRegexp(ValueError,'.*uid.*') as cm: + self.users.createInvite(0) + + + def test_getInfo(self): + self.assertEqual(None,self.users.getInfo(0)) if __name__ == '__main__': diff --git a/test/testWebApi.py b/test/testWebApi.py index b4ad2cd..b6cd72d 100644 --- a/test/testWebApi.py +++ b/test/testWebApi.py @@ -22,13 +22,17 @@ def getCount(self,info_hash): class MockUsers(object): def __init__(self): self._getInfo = {'numberOfTorrents' : 0, 'name':'aTestUser', 'password' : fairywren.USER_PASSWORD_FMT % 1} + self._addUser = None def getInfo(self,idNumber): return self._getInfo + + def addUser(self,username,password): + return self._addUser class MockAuth(object): def __init__(self): self._authenticateUser = 1 - self._addUser = None + self._isUserMemberOfRole = False def isUserMemberOfRole(self,userId,roles): @@ -37,8 +41,7 @@ def isUserMemberOfRole(self,userId,roles): def authenticateUser(self,username,password): return self._authenticateUser - def addUser(self,username,password): - return self._addUser + class MockTorrents(object): def __init__(self): @@ -458,7 +461,7 @@ def test_getself(self): class TestAddUser(AuthenticatedWebApiTest): def test_userAlreadyExists(self): - self.auth._addUser = None + self.users._addUser = None self.auth._isUserMemberOfRole = True try: self.urlopen('http://webapi/users', data= urllib.urlencode({'username':'foo','password':86*'0'})) @@ -470,17 +473,17 @@ def test_userAlreadyExists(self): self.assertTrue(False) def test_ok(self): - self.auth._addUser = 'FOO' + self.users._addUser = 'FOO' self.auth._isUserMemberOfRole = True r = self.urlopen('http://webapi/users', data= urllib.urlencode({'username':'foo','password':86*'0'})) self.assertEqual(200,r.code) r = json.loads(r.read()) self.assertIn('href',r) - self.assertEqual(r['href'],self.auth._addUser) + self.assertEqual(r['href'],self.users._addUser) def test_badPassword(self): - self.auth._addUser = 'meow' + self.users._addUser = 'meow' self.auth._isUserMemberOfRole = True try: self.urlopen('http://webapi/users', data= urllib.urlencode({'username':'foo','password':85*'0'})) @@ -493,7 +496,7 @@ def test_badPassword(self): self.assertTrue(False) def test_missingPassword(self): - self.auth._addUser = 'meow' + self.users._addUser = 'meow' self.auth._isUserMemberOfRole = True try: self.urlopen('http://webapi/users', data= urllib.urlencode({'username':'foo'})) diff --git a/users.py b/users.py index d76fbd2..f5170e9 100644 --- a/users.py +++ b/users.py @@ -1,20 +1,104 @@ import eventlet import fairywren +import hashlib +import psycopg2 +import logging +import os +import base64 class Users(object): + def __init__(self,salt): + self.salt = salt + self.connPool = None + self.log = logging.getLogger('fairywren.users') + self.log.info('Created') + def setConnectionPool(self,pool): self.connPool = pool + + def _saltPwhash(self,pwHash): + storedHash = hashlib.sha512() + storedHash.update(self.salt) + storedHash.update(pwHash) + return base64.urlsafe_b64encode(storedHash.digest()).replace('=','') + + def addUser(self,username,pwHash): + self.log.debug('Trying to add user %s',username) + secretKey = hashlib.sha512() - def getInfo(self,idNumber): + randomValue = os.urandom(1024) + secretKey.update(randomValue) + + saltedPw = self._saltPwhash(pwHash) + with self.connPool.item() as conn: cur = conn.cursor() - - cur.execute("Select users.name,count(torrents.creator) from users left join torrents on (torrents.creator=users.id) where users.id=%s group by users.name;",(idNumber,)) - result = cur.fetchone() - conn.rollback() + + try: + cur.execute("INSERT into users (name,password,secretKey) VALUES(%s,%s,%s) returning users.id;", + (username, + saltedPw, + base64.urlsafe_b64encode(secretKey.digest()).replace('=',''),) ) + except IntegrityError as e: + self.log.error(e) + conn.rollback() + cur.close() + return None + + conn.commit() + + newId, = cur.fetchone() cur.close() + conn.close() + self.log.debug('Added user, new id %.8x', newId) + return 'api/users/%.8x' % newId + + def createInvite(self,creatorId): + h = hashlib.md5() + h.update(os.urandom(1024)) + h.update(str(creatorId)) + + secret = h.digest() + h.update(h.digest()) + h.update(os.urandom(1024)) + secret += h.digest() + + with self.connPool.item() as conn: + cur = conn.cursor() + + try: + cur.execute("INSERT into invites (secret,inviter,creationdate) VALUES(%s,%s,timezone('UTC',CURRENT_TIMESTAMP));" , (base64.urlsafe_b64encode(secret).replace('=',''),creatorId,)) + except psycopg2.IntegrityError as e: + conn.rollback() + # 'foreign_key_violation' - violation of 'inviter' foreign key + # i.e. user with uid doesn't exist + if e.pgcode == '23503': + raise ValueError('User does not exist with that uid') + self.log.exception('Failed creating invite',exc_info=True) + raise e + except psycopg2.DatabaseError as e: + conn.rollback() + self.log.exception('Failed creating invite',exc_info=True) + raise e + conn.commit() + cur.close() + return secret + + def getInfo(self,idNumber): + with self.connPool.item() as conn: + cur = conn.cursor() + try: + cur.execute("Select users.name,count(torrents.creator) from users left join torrents on (torrents.creator=users.id) where users.id=%s group by users.name;",(idNumber,)) + result = cur.fetchone() + except psycopg2.DatabaseError as e: + self.log.exception('Failed getting info for user',exc_info=True) + raise e + finally: + cur.close() + conn.rollback() + retval = {} if result == None: return None diff --git a/webapi.py b/webapi.py index a4f41ab..6aef2e7 100644 --- a/webapi.py +++ b/webapi.py @@ -77,6 +77,11 @@ def authorizeUser(session,roles): def getResponseForSession(self,session): return {'my' : {'href':fairywren.USER_FMT % session.getId()} } + @requireAuthorization() + @resource(True,'POST','invites') + def createInvite(self,env,start_response,session): + return vanilla.sendJsonWsgiResponse(env,start_response,{'href':'foo'}) + @authorizeSelf(extractUserId) @requireAuthorization('Administrator') @parameter('password',decodePassword) @@ -110,7 +115,7 @@ def userInfo(self,env,start_response,session,uid): @resource(True,'POST','users') def addUser(self,env,start_response,session,password,username): - resourceForNewUser = self.authmgr.addUser(username,password) + resourceForNewUser = self.users.addUser(username,password) if resourceForNewUser == None: return vanilla.http_error(409,env,start_response,'user already exists') @@ -119,7 +124,6 @@ def addUser(self,env,start_response,session,password,username): return vanilla.sendJsonWsgiResponse(env,start_response,response) def searchTorrents(self,env,start_response,session,query): - tokens = query.get('token') if tokens == None: return vanilla.http_error(400,env,start_response,'search must have at least one instance of token parameter')