Skip to content

Commit

Permalink
Merge 19210c1 into 1312e0d
Browse files Browse the repository at this point in the history
  • Loading branch information
paulnues committed Jun 18, 2014
2 parents 1312e0d + 19210c1 commit d1a6cd0
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 22 deletions.
60 changes: 59 additions & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,34 @@ excluding the ``_id`` key/attribute.
if key in some_dict:
doc[key] = some_dict[key]

.. _overriding-authentication:

Overriding Authentication
-------------------------

Because each document is assigned to a specific database using the
`config_database` attribute, you may have the situation where a database
has its own specific credentials. In this case, you can override the
credentials of the :class:`~humbledb.mongo.Mongo` class by giving
the document its own credentials using the `config_auth` attribute.

.. rubric:: Example: Setting up a document with specific
authentication parameters

::

class AuthedDoc(Document):
config_database = 'authed_humble'
config_collection = 'authdoc'
config_auth = 'anotheruser:supersecret'

value = 'v'

with AuthedDB:
some_doc = AuthedDoc.find({AuthedDoc.value: 'foo'})

.. versionadded:: 5.2.0

.. _embedding-documents:

Embedding Documents
Expand Down Expand Up @@ -732,13 +760,28 @@ however you cannot nest a connection within itself (this will raise a
.. rubric:: Connection Settings

* **config_host** (``str``) - Hostname to connect to.
* **config_port** (``int``) - Port to connect to.
* **config_port** (``int``, optional, default:``27017``) - Port to connect to.

.. versionchanged:: 5.2.0

`config_port` is now optional and will default to what
:class:`~pymongo.connection.Connection` will use as default (port 27017).

* **config_replica** (``str``, optional) - Name of the replica set.

If ``config_replica`` is present on the class, then HumbleDB will automatically
use a :class:`~pymongo.connection.ReplicaSetConnection` for you. (Requires
``pymongo >= 2.1``.)

* **config_auth** (``str``, optional) - Username and password.

.. versionadded:: 5.2.0

The `config_auth` string format is identical to how the username and password are
found in a typical `MongoDB-URI
<http://docs.mongodb.org/manual/reference/connection-string/>`_ style
connection string format.

.. rubric:: Global Connection Settings

These settings are available globally through Pyconfig_ configuration keys. Use
Expand All @@ -757,6 +800,10 @@ either :func:`Pyconfig.set` (i.e. ``pyconfig.set('humbledb.connection_pool',
``use_greenlets`` with the :class:`~pymongo.connection.Connection`
instance. (This is only needed if you intend on using threading and greenlets
at the same time.)
* **humbledb.use_authentication** (``bool``, default: ``False``) - Whether to
use basic authentication, MONGODB-CR, with the
:class:`pymongo.connection.Connection` instance.
.. versionadded:: 5.2.0

More configuration settings are going to be added in the near future, so you
can customize your :class:`~pymongo.connection.Connection` to completely suit
Expand All @@ -779,6 +826,11 @@ your needs.
config_port = 3002
config_replica = 'RS1'
# An connection that uses Mongo-CR for basic authentication
class AuthedDB(Mongo):
config_host = 'anotherdb.example.com'
config_auth = 'someuser:secret'
# Use your custom subclasses as context managers
with MyDB:
docs = MyDoc.find({MyDoc.value: {'$gt': 3}})
Expand All @@ -800,6 +852,12 @@ your needs.
{'$push': {MyGroup.related: MyDoc._id},
multi=True)
# A Mongo subclass with authentication credentials will try to
# authenticate when needed.
with AuthedDB:
one_doc = AnotherDoc.find_one({AnotherDoc.value: 'a string'})
.. _reports:
Pre-aggregated Reports
Expand Down
3 changes: 3 additions & 0 deletions humbledb/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def __new__(mcs, name, bases, cls_dict):
member))
# Move the config to the page
page_dict[member] = cls_dict.pop(member)
# Add config_auth credentials to Page Document if available
if 'config_auth' in cls_dict:
page_dict['config_auth'] = cls_dict.pop('config_auth')
# Create our page subclass and assign to cls._page
cls_dict['_page'] = type(name + 'Page', (Page,), page_dict)
# Return our new Array
Expand Down
41 changes: 37 additions & 4 deletions humbledb/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from humbledb.mongo import Mongo
from humbledb.cursor import Cursor
from humbledb.maps import DictMap, NameMap, ListMap
from humbledb.errors import NoConnection, MissingConfig
from humbledb.errors import NoConnection, MissingConfig, InvalidAuth

COLLECTION_METHODS = set([_ for _ in dir(pymongo.collection.Collection) if not
_.startswith('_') and callable(getattr(pymongo.collection.Collection, _))])
Expand Down Expand Up @@ -120,7 +120,7 @@ def __new__(mcs, cls_name, bases, cls_dict):

# Attribute names that are configuration settings
config_names = set(['config_database', 'config_collection',
'config_indexes'])
'config_indexes', 'config_auth'])

# Attribute names that conflict with the dict base class
bad_names = mcs._collection_methods | set(['clear', 'collection',
Expand Down Expand Up @@ -207,6 +207,9 @@ def __getattr__(cls, name):

# See if we're looking for a collection method
if name in cls._collection_methods:
# Authenticate before attempting to connect to mongo.
cls.authenticate()

value = getattr(cls.collection, name, None)
if name in cls._wrapped_methods:
value = cls._wrap(value)
Expand All @@ -232,7 +235,7 @@ def _wrap(cls, func):
if func.__name__ in cls._wrapped_doc_methods:
@wraps(func)
def doc_wrapper(*args, **kwargs):
""" Wrapper function to gurantee object typing and indexes. """
""" Wrapper function to guarantee object typing and indexes. """
cls._ensure_indexes()
doc = func(*args, **kwargs)
# If doc is not iterable (e.g. None), then this will error
Expand Down Expand Up @@ -264,7 +267,11 @@ def _get_update(cls):
the collection method.
"""
return cls._update or cls.collection.update
if cls._update:
return cls._update

cls._authenticate()
return cls.collection.update

def _set_update(cls, value):
""" Allows setting the update attribute for testing with mocks. """
Expand Down Expand Up @@ -294,6 +301,7 @@ class Document(dict):
class BlogPost(Document):
config_database = 'db'
config_collection = 'example'
config_auth = 'user:pass' # optional auth credentials
meta = Embed('m')
meta.tags = 't'
Expand All @@ -316,6 +324,10 @@ class BlogPost(Document):
""" Collection name for this document. """
config_indexes = None
""" Indexes for this document. """
config_auth = None
""" Optional authentication credentials to use to connect to the database.
.. versionadded: 5.2
"""

def __repr__(self):
return "{}({})".format(
Expand Down Expand Up @@ -432,6 +444,7 @@ def _ensure_indexes(cls):
return

if cls.config_indexes:
cls.authenticate()
for index in cls.config_indexes:
logging.getLogger(__name__).info("Ensuring index: {}"
.format(index))
Expand All @@ -457,3 +470,23 @@ def _reload():
"""
cls._ensured = False

@classmethod
def authenticate(cls):
""" Authenticates wrapped document collection calls.
.. versionadded: 5.2
"""
if not Mongo.context:
raise NoConnection("Need connection to authenticate.")

username = None
password = None
if cls.config_auth:
try:
username, password = pymongo.uri_parser.parse_userinfo(
cls.config_auth)
except pymongo.errors.InvalidURI:
raise InvalidAuth('Invalid config_auth')

# Delegate authentication to HumbleDB connection class.
Mongo.context.authenticate(cls.config_database, username=username,
password=password)
6 changes: 5 additions & 1 deletion humbledb/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ class NestedConnection(RuntimeError):


class MissingConfig(RuntimeError):
""" Raised when configuartion is not configured correctly at runtime. """
""" Raised when configuration is not configured correctly at runtime. """


class InvalidAuth(RuntimeError):
""" Raised when a connection fails to authenticate to a database. """


def _import_pymongo_errors():
Expand Down
4 changes: 4 additions & 0 deletions humbledb/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def ensure(self, cls):
# Map the attribute name to its key name, or just let it ride
index = self._resolve_index(cls)

# Authenticate if necessary as ensure_index fails when called outside
# of the Mongo context manager's bounds.
cls.authenticate()

# Make the ensure index call
cls.collection.ensure_index(index, **self.kwargs)

Expand Down
84 changes: 77 additions & 7 deletions humbledb/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pytool.lang import classproperty, UNSET

from humbledb import _version
from humbledb.errors import NestedConnection
from humbledb.errors import (NestedConnection, MissingConfig, InvalidAuth)


__all__ = [
Expand Down Expand Up @@ -68,8 +68,13 @@ def __new__(mcs, name, bases, cls_dict):
if 'config_host' not in cls_dict or cls_dict['config_host'] is None:
raise TypeError("missing required 'config_host'")

if 'config_port' not in cls_dict or cls_dict['config_port'] is None:
raise TypeError("missing required 'config_port'")
# Validate that config_auth is acceptable
_config_auth = cls_dict.get('config_auth', None)
if _config_auth:
try:
creds = pymongo.uri_parser.parse_userinfo(str(_config_auth))
except pymongo.errors.InvalidURI:
raise TypeError("Invalid 'config_auth' value.")

# Create new class
cls = type.__new__(mcs, name, bases, cls_dict)
Expand Down Expand Up @@ -113,6 +118,49 @@ def reconnect(cls):
cls._connection.disconnect()
cls._connection = cls._new_connection()

def authenticate(cls, database, username=None, password=None):
""" Delegates authentication to be the responsibility of the
context manager.
.. versionadded: 5.2.0
"""
# Turning authentication off makes this call a noop
_config_use_auth = cls.config_use_authentication
if not _config_use_auth:
return

# Use Mongo class config_auth as defaults if no credentials are
# passed in.
if not username or not password:
auth = cls.config_auth
if not auth:
raise MissingConfig('Missing default config_auth.')

try:
username, password = pymongo.uri_parser.parse_userinfo(auth)
except pymongo.errors.InvalidURI:
raise InvalidAuth('Invalid config_auth.')

if _version._lt('2.5'):
valid = cls.connection[database].authenticate(username,
password)
if not valid:
raise InvalidAuth("Invalid database credentials.")
else:
try:
cls.connection[database].authenticate(username, password)
except pymongo.errors.PyMongoError:
raise InvalidAuth("Invalid database credentials.")

def logout(cls, database):
""" Explicitly deauthorizes the connection client from the database.
.. versionadded: 5.2
"""
if not cls._authenticated.get(database, False):
return

if cls._connection:
cls._connection[database].logout()

def __enter__(cls):
cls.start()

Expand Down Expand Up @@ -142,6 +190,7 @@ class Mongo(object):
class MyConnection(Mongo):
config_host = 'cluster1.mongo.mydomain.com'
config_port = 27017
config_auth = 'user:passwd'
Example usage::
Expand All @@ -155,8 +204,18 @@ class MyConnection(Mongo):
config_host = 'localhost'
""" The host name or address to connect to. """

config_port = 27017
""" The port to connect to. """
config_port = None
""" Optional default port to use if a port is not given. """

config_auth = None
""" Optional default authentication value to be for all connections
to the database if overriding credentials are not found in the
:class:`~humbledb.document.Document` config_auth attribute.
Authentication uses MONGODB-CR.
.. versionadded: 5.2
"""

config_replica = None
""" If you're connecting to a replica set, this holds its name. """
Expand Down Expand Up @@ -188,6 +247,12 @@ class MyConnection(Mongo):
"""

config_use_authentication = pyconfig.setting('humbledb.use_authentication',
False)
""" This specifies if connections should use authentication or not.
.. versionadded: 5.2
"""

def __new__(cls):
""" This class cannot be instantiated. """
return cls
Expand All @@ -197,21 +262,26 @@ def _new_connection(cls):
""" Return a new connection to this class' database. """
kwargs = {
'host': cls.config_host,
'port': cls.config_port,
'max_pool_size': cls.config_max_pool_size,
'auto_start_request': cls.config_auto_start_request,
'use_greenlets': cls.config_use_greenlets,
'tz_aware': cls.config_tz_aware,
'w': cls.config_write_concern,
}
if cls.config_port:
kwargs['port'] = cls.config_port

if cls.config_replica:
kwargs['replicaSet'] = cls.config_replica
logging.getLogger(__name__).info("Creating new MongoDB connection "
"to '{}:{}' replica: {}".format(cls.config_host,
cls.config_port, cls.config_replica))
else:
db_location = '{}:{}'.format(cls.config_port,
cls.config_host) if cls.config_port else '{}'.format(
cls.config_host)
logging.getLogger(__name__).info("Creating new MongoDB connection "
"to '{}:{}'".format(cls.config_host, cls.config_port))
"to '{}'".format(db_location))

return cls.config_connection_cls(**kwargs)

Expand Down
3 changes: 2 additions & 1 deletion test/test_humbledb/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
class TestArray(Array):
config_database = database_name()
config_collection = 'arrays'
config_auth = 'authuser:pass1'
config_max_size = 3
config_padding = 100


def teardown():
DBTest.connection.drop_database(database_name())
DBTest.connection[database_name()].drop_collection('arrays')


def _word():
Expand Down
Loading

0 comments on commit d1a6cd0

Please sign in to comment.