Skip to content

Commit

Permalink
Merge branch 'truemped-session-api'
Browse files Browse the repository at this point in the history
Fixes GH-33
  • Loading branch information
nailor committed Jan 15, 2012
2 parents 6745a57 + 10104a8 commit 2736ec0
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 11 deletions.
52 changes: 52 additions & 0 deletions doc/python-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
========
Expand Down
8 changes: 8 additions & 0 deletions test/conf/local_session.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[couch_httpd_auth]
secret = bd42ab447cdaecb52f2b2dc3bda6ec10

[httpd]
port = 8922

[admins]
admin = -hashed-609ab15a7189304d14390b48876180f498a38008,35cee0c36d7a4bd5f1ba460eda70454f
65 changes: 55 additions & 10 deletions test/couch_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -56,24 +59,66 @@ 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)
# Wait for couchdb to start

while True:
try:
f = request.urlopen(baseurl)
f = request.urlopen('http://localhost:%s' % port)
except URLError:
continue
try:
Expand Down Expand Up @@ -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:
Expand Down
Empty file added test/test_session/__init__.py
Empty file.
110 changes: 110 additions & 0 deletions test/test_session/test_session.py
Original file line number Diff line number Diff line change
@@ -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})
2 changes: 1 addition & 1 deletion test/test_usermgmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
46 changes: 46 additions & 0 deletions trombi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import re
import collections
import tornado.ioloop
import urllib

try:
# Python 3
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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

This comment has been minimized.

Copy link
@akheron

akheron Jan 16, 2012

self.sesison_cookie looks like a typo :)

This comment has been minimized.

Copy link
@truemped

truemped Jan 16, 2012

Contributor

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


self._client.fetch(*args, **fetch_args)

def create(self, name, callback):
Expand Down Expand Up @@ -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):
Expand Down

1 comment on commit 2736ec0

@nailor
Copy link
Member Author

@nailor nailor commented on 2736ec0 Jan 16, 2012

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.