Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'truemped-session-api'

Fixes GH-33
  • Loading branch information...
commit 2736ec0135853f64405392643784bf7f61820418 2 parents 6745a57 + 10104a8
@nailor nailor authored
View
52 doc/python-api.rst
@@ -235,6 +235,58 @@ argument.
Lists available databases. On success, calls *callback* with a
generator object containing all databases.
+ .. method:: add_user(name, password, callback, doc=None)
+
+ Add a user with *name* and *password* to the *_users* database.
+ On success, calls *callback* with the users :class:`Document`.
+ If you want to store additional attributes in the user's
+ document, provide them as a *doc* dict.
+
+ .. method:: get_user(name, callback, attachments=False)
+
+ Load the user's document identified by *name*. Optionally
+ retrieve the *attachments*.
+
+ .. method:: update_user(user_doc, callback)
+
+ Update the document for the user. On success, *callback* is
+ called with the new :class:`Document`.
+
+ .. method:: update_user_password(username, password, callback)
+
+ Only update the user's password. On success, *callback* is
+ called with the new :class:`Document`.
+
+ .. method:: delete_user(user_doc, callback)
+
+ Delete a user from the CouchDB database. On success, *callback*
+ will be called with :class:`Database` as an argument.
+
+ .. method:: login(username, password, callback)
+
+ This method performs a login against `CouchDB session API`_ using
+ *username* and *password*. On succesfull login session cookie is
+ stored for subsequent requests and *callback* is called with
+ :class:`TrombiResult` as an argument.
+
+ Note, that the username and password are sent unencrypted on the
+ wire, so this method should be used either in fully trusted
+ network or over HTTPS connection.
+
+ .. method:: logout(callback)
+
+ This method performs a logout against `CouchDB session API`_. If
+ logout is succesfull, old session cookie is no longer used on
+ subsequent requests and *callback* is called with
+ :class:`TrombiResult` instance as an argument.
+
+ .. method:: session(callback)
+
+ This method fetches login details from `CouchDB Session API`_.
+ On success the *callback* is called with :class:`TrombiResult`
+ instance as an argument.
+
+ .. _CouchDB session API: http://wiki.apache.org/couchdb/Session_API
Database
========
View
8 test/conf/local_session.ini
@@ -0,0 +1,8 @@
+[couch_httpd_auth]
+secret = bd42ab447cdaecb52f2b2dc3bda6ec10
+
+[httpd]
+port = 8922
+
+[admins]
+admin = -hashed-609ab15a7189304d14390b48876180f498a38008,35cee0c36d7a4bd5f1ba460eda70454f
View
65 test/couch_util.py
@@ -39,11 +39,14 @@
from urllib2 import URLError
import nose.tools
-from tornado.httpclient import HTTPClient
+from tornado.httpclient import HTTPClient, HTTPError
baseurl = ''
-def setup():
+_proc = None
+
+
+def setup_with_admin():
global _proc, baseurl
try:
shutil.rmtree('tmp')
@@ -56,16 +59,58 @@ def setup():
os.mkdir('tmp')
os.mkdir('tmp/couch')
- dbdir = 'tmp/couch'
- ini = 'tmp/local.ini'
- log = 'tmp/couch.log'
+ port = 8922
+ baseurl = 'http://localhost:%d/' % (port)
+
+ dir = os.path.dirname(__file__)
+
+ cmdline = 'couchdb -n -a %s -a %s' % (
+ os.path.join(dir, 'conf/local.ini'),
+ os.path.join(dir, 'conf/local_session.ini'),
+ )
+
+ null = open('/dev/null', 'w')
+ _proc = subprocess.Popen(
+ cmdline, shell=True, stdout=null, stderr=null
+ )
+
+ # Wait for couchdb to start
+ time.sleep(1)
+ # Wait for couchdb to start
+
+ while True:
+ try:
+ f = request.urlopen('http://localhost:%s' % port)
+ except URLError:
+ continue
+ try:
+ json.loads(f.read().decode('utf-8'))
+ except ValueError:
+ continue
+ # Got a sensible response
+ break
+
+
+def setup():
+ global _proc, baseurl
+ try:
+ shutil.rmtree('tmp')
+ except OSError:
+ # Python 3
+ err = sys.exc_info()[1]
+ if err.errno != errno.ENOENT:
+ raise
+
+ os.mkdir('tmp')
+ os.mkdir('tmp/couch')
port = 8921
- baseurl = 'http://localhost:%d/' % port
+ baseurl = 'http://localhost:%d/' % (port)
- cmdline = 'couchdb -n -a test/conf/local.ini'
+ dir = os.path.dirname(__file__)
+ cmdline = 'couchdb -n -a %s' % os.path.join(dir, 'conf/local.ini')
null = open('/dev/null', 'w')
- _proc = subprocess.Popen(cmdline, shell=True)#, stdout=null, stderr=null)
+ _proc = subprocess.Popen(cmdline, shell=True, stdout=null, stderr=null)
# Wait for couchdb to start
time.sleep(1)
@@ -73,7 +118,7 @@ def setup():
while True:
try:
- f = request.urlopen(baseurl)
+ f = request.urlopen('http://localhost:%s' % port)
except URLError:
continue
try:
@@ -191,7 +236,7 @@ def inner(*args, **kwargs):
dbs = json.loads(response.body.decode('utf-8'))
except ValueError:
print >> sys.stderr, \
- "CouchDB's response was invalid JSON: %s" % db_string
+ "CouchDB's response was invalid JSON: %s" % response.body
sys.exit(2)
for database in dbs:
View
0  test/test_session/__init__.py
No changes.
View
110 test/test_session/test_session.py
@@ -0,0 +1,110 @@
+#
+# Copyright (c) 2011 Daniel Truemper truemped@googlemail.com
+#
+# test_session.py 13-Oct-2011
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use, copy,
+# modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from __future__ import with_statement
+
+from nose.tools import eq_ as eq
+
+from ..couch_util import setup_with_admin as setup, teardown, with_couchdb
+from ..util import with_ioloop
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+try:
+ # Python 3
+ from urllib.request import urlopen
+ from urllib.error import HTTPError
+except ImportError:
+ # Python 2
+ from urllib2 import urlopen
+ from urllib2 import HTTPError
+
+import trombi
+import trombi.errors
+
+
+@with_ioloop
+@with_couchdb
+def test_session_api_with_wrong_credentials(baseurl, ioloop):
+ s = trombi.Server(baseurl, io_loop=ioloop)
+
+ def session_callback(response):
+ assert response.error
+ eq(response.msg, 'Name or password is incorrect.')
+ ioloop.stop()
+
+ s.login(username="daniel", password="daniel", callback=session_callback)
+ ioloop.start()
+
+
+@with_ioloop
+@with_couchdb
+def test_session_with_user(baseurl, ioloop):
+ s = trombi.Server(baseurl, io_loop=ioloop)
+ result = {}
+
+ def session_callback(session_info):
+ result['session_info'] = session_info
+ ioloop.stop()
+
+ def add_user_callback(response):
+ assert not response.error
+ ioloop.stop()
+
+ # add a user
+ s.add_user('testuser', 'testpassword', add_user_callback)
+ ioloop.start()
+
+ # login
+ s.login(username="testuser", password="testpassword",
+ callback=session_callback)
+ ioloop.start()
+
+ # check for the cookie and user info
+ eq(result['session_info'].content, {u'ok': True, u'name': u'testuser',
+ u'roles': []})
+ assert s.session_cookie.startswith('AuthSession')
+
+ # get the session info
+ s.session(session_callback)
+ ioloop.start()
+
+ # check that no cookie has been sent and the session info is correct
+ eq(result['session_info'].content,
+ {u'info': {u'authentication_handlers':
+ [u'oauth', u'cookie', u'default'], u'authentication_db':
+ u'_users'}, u'userCtx': {u'name': None, u'roles': []},
+ u'ok':
+ True})
+
+ # check that logout is working
+ s.logout(session_callback)
+ ioloop.start()
+
+ assert not s.session_cookie
+ eq(result['session_info'].content, {u'ok': True})
View
2  test/test_usermgmt.py
@@ -30,7 +30,7 @@
import sys
from nose.tools import eq_ as eq
-from .couch_util import setup as setup, teardown, with_couchdb
+from .couch_util import setup, teardown, with_couchdb
from .util import with_ioloop, DatetimeEncoder
try:
View
46 trombi/client.py
@@ -30,6 +30,7 @@
import re
import collections
import tornado.ioloop
+import urllib
try:
# Python 3
@@ -145,6 +146,7 @@ class Server(TrombiObject):
def __init__(self, baseurl, fetch_args=None, io_loop=None,
json_encoder=None, **client_args):
self.error = False
+ self.session_cookie = None
self.baseurl = baseurl
if self.baseurl[-1] == '/':
self.baseurl = self.baseurl[:-1]
@@ -177,6 +179,14 @@ def _fetch(self, *args, **kwargs):
}
fetch_args.update(self._fetch_args)
fetch_args.update(kwargs)
+
+ if self.session_cookie:
+ fetch_args['X-CouchDB-WWW-Authenticate': 'Cookie']
+ if 'Cookie' in fetch_args:
+ fetch_args['Cookie'] += '; %s' % self.session_cookie
+ else:
+ fetch_args['Cookie'] = self.sesison_cookie
@akheron Collaborator
akheron added a note

self.sesison_cookie looks like a typo :)

Ups, indeed! Strange, that the integration test passes! Looks like the test is not that accurate :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
self._client.fetch(*args, **fetch_args)
def create(self, name, callback):
@@ -304,6 +314,42 @@ def delete_user(self, user_doc, callback):
userdb = Database(self, '_users')
userdb.delete(user_doc, callback)
+ def logout(self, callback):
+ def _really_callback(response):
+ if response.code == 200:
+ self.session_cookie = None
+ callback(TrombiResult(json.loads(response.body)))
+ else:
+ callback(_error_response(response))
+
+ url = '%s/%s' % (self.baseurl, '_session')
+ self._client.fetch(url, _really_callback, method='DELETE')
+
+ def login(self, username, password, callback):
+ def _really_callback(response):
+ if response.code in (200, 302):
+ self.session_cookie = response.headers['Set-Cookie']
+ response_body = json.loads(response.body)
+ callback(TrombiResult(response_body))
+ else:
+ callback(_error_response(response))
+
+ body = urllib.urlencode({'name': username, 'password': password})
+ url = '%s/%s' % (self.baseurl, '_session')
+
+ self._client.fetch(url, _really_callback, method='POST', body=body)
+
+ def session(self, callback):
+ def _really_callback(response):
+ if response.code == 200:
+ body = json.loads(response.body)
+ callback(TrombiResult(body))
+ else:
+ callback(_error_response(response))
+
+ url = '%s/%s' % (self.baseurl, '_session')
+ self._client.fetch(url, _really_callback)
+
class Database(TrombiObject):
def __init__(self, server, name):

1 comment on commit 2736ec0

@nailor
Collaborator

Oh snap. Yeah, that's definitely a typo. I blame the nightly hours I used to hack this together. Will fix ASAP!

Please sign in to comment.
Something went wrong with that request. Please try again.