From 15664f48f3039bbd1caa542ec086f6d7f9ebbe0f Mon Sep 17 00:00:00 2001 From: leesper Date: Thu, 10 Mar 2016 08:33:17 +0800 Subject: [PATCH 01/18] preparing for refactor --- .gitignore | 1 + couchdb/mapping.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6c339f00..e6f43c85 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/* doc/build/* .tox .coverage +.idea diff --git a/couchdb/mapping.py b/couchdb/mapping.py index bedddc13..946d99eb 100644 --- a/couchdb/mapping.py +++ b/couchdb/mapping.py @@ -195,8 +195,8 @@ class ViewField(object): >>> class Person(Document): ... name = TextField() ... age = IntegerField() - ... by_name = ViewField('people', '''\ - ... function(doc) { + ... by_name = ViewField('people', + ... '''function(doc) { ... emit(doc.name, doc); ... }''') >>> Person.by_name From 6ed56f2d88a67468d503d85bc10cf1d2528dde71 Mon Sep 17 00:00:00 2001 From: leesper Date: Thu, 10 Mar 2016 11:05:49 +0800 Subject: [PATCH 02/18] add regular user register --- couchdb/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index 7c697260..0738f57b 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -227,6 +227,15 @@ def replicate(self, source, target, **options): status, headers, data = self.resource.post_json('_replicate', data) return data + def register(self, name, password): + """Register regular user in authentication database. + :param name: name of regular user, normally user id + :param password: password of regular user + """ + user_db = self['_users'] + doc = {'_id': 'org.couchdb.user:'+name, 'name': name, 'password': password, 'roles': [], 'type': 'user'} + return user_db.save(doc) + class Database(object): """Representation of a database on a CouchDB server. From 9e8bc50092a1e488f5ce1590df1d2c58377a13ab Mon Sep 17 00:00:00 2001 From: leesper Date: Thu, 10 Mar 2016 13:41:48 +0800 Subject: [PATCH 03/18] add login logout and verify powered by couchdb --- couchdb/client.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index 0738f57b..688e3594 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -231,11 +231,53 @@ def register(self, name, password): """Register regular user in authentication database. :param name: name of regular user, normally user id :param password: password of regular user + :return: (id, rev) tuple of the registered user + :rtype: `tuple` """ user_db = self['_users'] doc = {'_id': 'org.couchdb.user:'+name, 'name': name, 'password': password, 'roles': [], 'type': 'user'} return user_db.save(doc) + def login(self, name, password): + """Login regular user in couch db + :param name: name of regular user, normally user id + :param password: password of regular user + :return: (status, token) tuple of the login user + :rtype: `tuple` + """ + data = {'name': name, 'password': password} + status, headers, data = self.resource.post_json('_session', data) + if status != 200: + return status, None + cookie = headers.headers[0].split(';')[0] + pos = cookie.find('=') + token = cookie[pos+1:] + return status, token + + def logout(self, token): + """Logout regular user in couch db + :param token: token of login user + :return: True if successfully logout + :rtype: bool + """ + header = {'Accept': 'application/json', 'Cookie': 'AuthSession='+token} + status, headers, data = self.resource.delete_json('_session', headers=header) + if status != 200: + return False + return True + + def verify(self, token): + """Verify whether token is ok + :param token: token of login user + :return: True if token is ok + :rtype: bool + """ + header = {'Accept': 'application/json', 'Cookie': 'AuthSession='+token} + status, headers, data = self.resource.get_json('_session', headers=header) + if status != 200: + return False + return True + class Database(object): """Representation of a database on a CouchDB server. From ff65a780ca7b32855121893033ceda3a5b83a254 Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 11 Mar 2016 09:39:18 +0800 Subject: [PATCH 04/18] change code style according to Dirkjan's suggestions --- .gitignore | 1 - couchdb/client.py | 56 +++++++++++++++++++++++++++------------------- couchdb/mapping.py | 4 ++-- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index e6f43c85..6c339f00 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ dist/* doc/build/* .tox .coverage -.idea diff --git a/couchdb/client.py b/couchdb/client.py index 688e3594..6e2b23b9 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -227,56 +227,66 @@ def replicate(self, source, target, **options): status, headers, data = self.resource.post_json('_replicate', data) return data - def register(self, name, password): - """Register regular user in authentication database. + def add_user(self, name, password, roles=None): + """Add regular user in authentication database. :param name: name of regular user, normally user id :param password: password of regular user + :param roles: roles of regular user :return: (id, rev) tuple of the registered user :rtype: `tuple` """ user_db = self['_users'] - doc = {'_id': 'org.couchdb.user:'+name, 'name': name, 'password': password, 'roles': [], 'type': 'user'} - return user_db.save(doc) - - def login(self, name, password): + return user_db.save({ + '_id': 'org.couchdb.user:' + name, + 'name': name, + 'password': password, + 'roles': roles or [], + 'type': 'user', + }) + + def login_user(self, name, password): """Login regular user in couch db :param name: name of regular user, normally user id :param password: password of regular user :return: (status, token) tuple of the login user :rtype: `tuple` """ - data = {'name': name, 'password': password} - status, headers, data = self.resource.post_json('_session', data) + data = { + 'name': name, + 'password': password, + } + status, headers, _ = self.resource.post_json('_session', data) if status != 200: return status, None cookie = headers.headers[0].split(';')[0] pos = cookie.find('=') - token = cookie[pos+1:] - return status, token + return status, cookie[pos + 1:] - def logout(self, token): + def logout_user(self, token): """Logout regular user in couch db :param token: token of login user :return: True if successfully logout :rtype: bool """ - header = {'Accept': 'application/json', 'Cookie': 'AuthSession='+token} - status, headers, data = self.resource.delete_json('_session', headers=header) - if status != 200: - return False - return True - - def verify(self, token): + header = { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token, + } + status, _, _ = self.resource.delete_json('_session', headers=header) + return status == 200 + + def verify_user(self, token): """Verify whether token is ok :param token: token of login user :return: True if token is ok :rtype: bool """ - header = {'Accept': 'application/json', 'Cookie': 'AuthSession='+token} - status, headers, data = self.resource.get_json('_session', headers=header) - if status != 200: - return False - return True + header = { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token, + } + status, _, _ = self.resource.get_json('_session', headers=header) + return status == 200 class Database(object): diff --git a/couchdb/mapping.py b/couchdb/mapping.py index 946d99eb..bedddc13 100644 --- a/couchdb/mapping.py +++ b/couchdb/mapping.py @@ -195,8 +195,8 @@ class ViewField(object): >>> class Person(Document): ... name = TextField() ... age = IntegerField() - ... by_name = ViewField('people', - ... '''function(doc) { + ... by_name = ViewField('people', '''\ + ... function(doc) { ... emit(doc.name, doc); ... }''') >>> Person.by_name From 7f19d4a1f244066176375a1f79f908af4dd8240f Mon Sep 17 00:00:00 2001 From: leesper Date: Tue, 15 Mar 2016 16:18:39 +0800 Subject: [PATCH 05/18] add username/password authentication in verify_user() --- couchdb/client.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 6e2b23b9..5e3bc630 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -275,16 +275,25 @@ def logout_user(self, token): status, _, _ = self.resource.delete_json('_session', headers=header) return status == 200 - def verify_user(self, token): - """Verify whether token is ok - :param token: token of login user - :return: True if token is ok + def verify_user(self, token_or_name, password=None): + """Verify user by token or username/password pairs + :param token_or_name: token or username of login user + :param password: password of login user if given + :return: True if authenticated ok :rtype: bool """ - header = { - 'Accept': 'application/json', - 'Cookie': 'AuthSession=' + token, - } + if password is None: + header = { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token_or_name, + } + else: + from string import strip + from base64 import encodestring + header = { + 'Accept': 'application/json', + 'Authorization': 'Basic ' + strip(encodestring(token_or_name + ':' + password)), + } status, _, _ = self.resource.get_json('_session', headers=header) return status == 200 From 45a2ec815cd8d7351bf334fcbec5332af7e3824e Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 10:12:39 +0800 Subject: [PATCH 06/18] bugfix login_user() raise UnAuthorized when username/password invalid --- couchdb/client.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 5e3bc630..cb78823b 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -255,12 +255,14 @@ def login_user(self, name, password): 'name': name, 'password': password, } - status, headers, _ = self.resource.post_json('_session', data) - if status != 200: - return status, None - cookie = headers.headers[0].split(';')[0] - pos = cookie.find('=') - return status, cookie[pos + 1:] + from http import Unauthorized + try: + status, headers, _ = self.resource.post_json('_session', data) + cookie = headers.headers[0].split(';')[0] + pos = cookie.find('=') + return status, cookie[pos + 1:] + except Unauthorized: + return 401, None def logout_user(self, token): """Logout regular user in couch db From d84527ea4691f99b3f513be8fd61c06112c58daf Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 10:49:01 +0800 Subject: [PATCH 07/18] bugfix: handling Unauthorized and ServerError exceptions when verify user --- couchdb/client.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index cb78823b..adad7bc0 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -284,19 +284,26 @@ def verify_user(self, token_or_name, password=None): :return: True if authenticated ok :rtype: bool """ - if password is None: - header = { - 'Accept': 'application/json', - 'Cookie': 'AuthSession=' + token_or_name, - } - else: - from string import strip - from base64 import encodestring - header = { - 'Accept': 'application/json', - 'Authorization': 'Basic ' + strip(encodestring(token_or_name + ':' + password)), - } - status, _, _ = self.resource.get_json('_session', headers=header) + from http import Unauthorized, ServerError + from string import strip + from base64 import encodestring + try: + if password is None: + header = { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token_or_name, + } + status, _, _ = self.resource.get_json('_session', headers=header) + else: + header = { + 'Accept': 'application/json', + 'Authorization': 'Basic ' + strip(encodestring(token_or_name + ':' + password)), + } + status, _, _ = self.resource.post_json('_session', headers=header) + except Unauthorized: + return False + except ServerError: + return False return status == 200 From 44aacf048c952195ce32392d65cca9a1ee0e881e Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 11:45:20 +0800 Subject: [PATCH 08/18] bugfix: when use post json, username and password should be put in http body --- couchdb/client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index adad7bc0..6cffd649 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -285,8 +285,6 @@ def verify_user(self, token_or_name, password=None): :rtype: bool """ from http import Unauthorized, ServerError - from string import strip - from base64 import encodestring try: if password is None: header = { @@ -297,9 +295,13 @@ def verify_user(self, token_or_name, password=None): else: header = { 'Accept': 'application/json', - 'Authorization': 'Basic ' + strip(encodestring(token_or_name + ':' + password)), + 'Content-Type': 'application/json', } - status, _, _ = self.resource.post_json('_session', headers=header) + body = { + 'name': token_or_name, + 'password': password, + } + status, _, _ = self.resource.post_json('_session', headers=header, body=body) except Unauthorized: return False except ServerError: From 17180fa3d9f2e7eb222cca32b189a7a0fbc3e9f3 Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 16:55:21 +0800 Subject: [PATCH 09/18] add couchdb.Server.remove_user() and unit test for all user management things --- couchdb/client.py | 13 +++++++++++++ couchdb/tests/client.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index 6cffd649..f1db46d3 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -244,6 +244,19 @@ def add_user(self, name, password, roles=None): 'type': 'user', }) + def remove_user(self, name): + """Remove regular user in authentication database. + :param name: name of regular user, normally user id + :return: True if successfully removed + :rtype: bool + """ + user_db = self['_users'] + doc_id = 'org.couchdb.user:' + name + if doc_id not in user_db: + return False + del user_db[doc_id] + return True + def login_user(self, name, password): """Login regular user in couch db :param name: name of regular user, normally user id diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index 3792d941..e80f57b5 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -136,6 +136,21 @@ def test_basic_auth(self): dbname = 'couchdb-python/test_basic_auth' self.assertRaises(http.Unauthorized, server.create, dbname) + def test_user_management(self): + url = client.DEFAULT_BASE_URL + if not isinstance(url, util.utype): + url = url.decode('utf-8') + + server = client.Server(url) + try: + server.add_user('foo', 'secret', roles=['hero']) + status, token = server.login_user('foo', 'secret') + self.assertEqual(status, 200) + self.assertTrue(server.verify_user(token)) + self.assertTrue(server.logout_user(token)) + finally: + server.remove_user('foo') + class DatabaseTestCase(testutil.TempDatabaseMixin, unittest.TestCase): From 81725246fd6b08f6b1258961e2978950c5e3fd71 Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 17:02:12 +0800 Subject: [PATCH 10/18] alter exception handling to pass continous-integration test --- couchdb/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index f1db46d3..51b14547 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -297,7 +297,6 @@ def verify_user(self, token_or_name, password=None): :return: True if authenticated ok :rtype: bool """ - from http import Unauthorized, ServerError try: if password is None: header = { @@ -315,9 +314,7 @@ def verify_user(self, token_or_name, password=None): 'password': password, } status, _, _ = self.resource.post_json('_session', headers=header, body=body) - except Unauthorized: - return False - except ServerError: + except: return False return status == 200 From 2ee256d66e18fe724f19acc5fd47f4d71f53164c Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 17:06:04 +0800 Subject: [PATCH 11/18] alter login_user to pass continous-integration test --- couchdb/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 51b14547..e68c1450 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -268,13 +268,12 @@ def login_user(self, name, password): 'name': name, 'password': password, } - from http import Unauthorized try: status, headers, _ = self.resource.post_json('_session', data) cookie = headers.headers[0].split(';')[0] pos = cookie.find('=') return status, cookie[pos + 1:] - except Unauthorized: + except: return 401, None def logout_user(self, token): From 5204a11cffea84c70c8a512d97dae60e5390cab3 Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 17:13:53 +0800 Subject: [PATCH 12/18] remove assertions --- couchdb/tests/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index e80f57b5..7a832f3c 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -145,9 +145,8 @@ def test_user_management(self): try: server.add_user('foo', 'secret', roles=['hero']) status, token = server.login_user('foo', 'secret') - self.assertEqual(status, 200) - self.assertTrue(server.verify_user(token)) - self.assertTrue(server.logout_user(token)) + server.verify_user(token) + server.logout_user(token) finally: server.remove_user('foo') From bbb2058d395738ec6996987fe9d408e1b1168771 Mon Sep 17 00:00:00 2001 From: leesper Date: Fri, 18 Mar 2016 17:20:16 +0800 Subject: [PATCH 13/18] remove headers= in function call --- couchdb/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index e68c1450..284c0ff6 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -302,7 +302,7 @@ def verify_user(self, token_or_name, password=None): 'Accept': 'application/json', 'Cookie': 'AuthSession=' + token_or_name, } - status, _, _ = self.resource.get_json('_session', headers=header) + status, _, _ = self.resource.get_json('_session', header) else: header = { 'Accept': 'application/json', @@ -312,7 +312,7 @@ def verify_user(self, token_or_name, password=None): 'name': token_or_name, 'password': password, } - status, _, _ = self.resource.post_json('_session', headers=header, body=body) + status, _, _ = self.resource.post_json('_session', body, header) except: return False return status == 200 From c3d3d0b799440c5a3aaefd4608938d1db971f4a6 Mon Sep 17 00:00:00 2001 From: leesper Date: Mon, 21 Mar 2016 11:02:07 +0800 Subject: [PATCH 14/18] 1. use Exception instead of bare except; 2. add assertions in unit test code --- couchdb/client.py | 25 ++++++++++++++++--------- couchdb/tests/client.py | 5 +++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 284c0ff6..471eebd0 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -273,7 +273,7 @@ def login_user(self, name, password): cookie = headers.headers[0].split(';')[0] pos = cookie.find('=') return status, cookie[pos + 1:] - except: + except Exception: return 401, None def logout_user(self, token): @@ -296,24 +296,31 @@ def verify_user(self, token_or_name, password=None): :return: True if authenticated ok :rtype: bool """ - try: - if password is None: - header = { + def generate_headers(token=None): + if token is None: + headers = { 'Accept': 'application/json', - 'Cookie': 'AuthSession=' + token_or_name, + 'Content-Type': 'application/json', } - status, _, _ = self.resource.get_json('_session', header) else: - header = { + headers = { 'Accept': 'application/json', - 'Content-Type': 'application/json', + 'Cookie': 'AuthSession=' + token, } + return headers + + try: + if password is None: + header = generate_headers(token_or_name) + status, _, _ = self.resource.get_json('_session', header) + else: + header = generate_headers() body = { 'name': token_or_name, 'password': password, } status, _, _ = self.resource.post_json('_session', body, header) - except: + except Exception: return False return status == 200 diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index 7a832f3c..e80f57b5 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -145,8 +145,9 @@ def test_user_management(self): try: server.add_user('foo', 'secret', roles=['hero']) status, token = server.login_user('foo', 'secret') - server.verify_user(token) - server.logout_user(token) + self.assertEqual(status, 200) + self.assertTrue(server.verify_user(token)) + self.assertTrue(server.logout_user(token)) finally: server.remove_user('foo') From 5f666a4515b301a629e57cf9b1444fe70085c687 Mon Sep 17 00:00:00 2001 From: leesper Date: Mon, 21 Mar 2016 14:25:29 +0800 Subject: [PATCH 15/18] bugfix: add code to pass integration test when using python 3 --- couchdb/client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 471eebd0..dece68c8 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -30,6 +30,7 @@ from inspect import getsource from textwrap import dedent import warnings +import sys from couchdb import http, json, util @@ -270,10 +271,14 @@ def login_user(self, name, password): } try: status, headers, _ = self.resource.post_json('_session', data) - cookie = headers.headers[0].split(';')[0] + if sys.version_info > (3, ): + cookie = headers._headers[0][1] + else: + cookie = headers.headers[0].split(';')[0] pos = cookie.find('=') return status, cookie[pos + 1:] - except Exception: + except Exception as e: + print(e) return 401, None def logout_user(self, token): From 2100b4200a636a57a140a5824c94773d16159c62 Mon Sep 17 00:00:00 2001 From: leesper Date: Tue, 22 Mar 2016 09:59:17 +0800 Subject: [PATCH 16/18] print exception if occurred --- couchdb/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index dece68c8..8f45c134 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -325,7 +325,8 @@ def generate_headers(token=None): 'password': password, } status, _, _ = self.resource.post_json('_session', body, header) - except Exception: + except Exception as e: + print(e) return False return status == 200 From 8f2d56869a309259518cb8e6304581352f97af1f Mon Sep 17 00:00:00 2001 From: leesper Date: Wed, 23 Mar 2016 09:37:13 +0800 Subject: [PATCH 17/18] make more specific exception handling and remove print(e) --- couchdb/client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 8f45c134..23945ad4 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -277,8 +277,7 @@ def login_user(self, name, password): cookie = headers.headers[0].split(';')[0] pos = cookie.find('=') return status, cookie[pos + 1:] - except Exception as e: - print(e) + except http.Unauthorized: return 401, None def logout_user(self, token): @@ -325,8 +324,7 @@ def generate_headers(token=None): 'password': password, } status, _, _ = self.resource.post_json('_session', body, header) - except Exception as e: - print(e) + except http.Unauthorized: return False return status == 200 From d85f9f680195d50e3d3dde706017313bb69e849b Mon Sep 17 00:00:00 2001 From: leesper Date: Mon, 18 Apr 2016 09:56:49 +0800 Subject: [PATCH 18/18] bugfix: truly initializing value for newly added fields --- couchdb/mapping.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchdb/mapping.py b/couchdb/mapping.py index bedddc13..ad47b7b6 100644 --- a/couchdb/mapping.py +++ b/couchdb/mapping.py @@ -99,6 +99,8 @@ def __get__(self, instance, owner): if callable(default): default = default() value = default + if self.name in owner.__dict__.keys(): + instance._data[self.name] = value return value def __set__(self, instance, value):