Skip to content
This repository was archived by the owner on Oct 23, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ setuptools>=2.1
sphinx_rtd_theme
tox>=1.7.1
wheel>=0.22.0
git+https://github.com/mverteuil/pytest-ipdb.git
3 changes: 2 additions & 1 deletion objectrocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
# Import this object for ease of access.
from objectrocket.client import Client # noqa

__version__ = '0.2.0b'
VERSION = ('0', '3', '0-beta')
__version__ = '.'.join(VERSION)
71 changes: 71 additions & 0 deletions objectrocket/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Authentication operations."""
import functools
import requests

from objectrocket import operations
from objectrocket import errors


def _token_auto_auth(func):
"""Wrap class methods with automatic token re-authentication.

This wrapper will detect authentication failures coming from its wrapped method. When one is
caught, it will request a new token, and simply replay the original request. If the client
object is not using token authentication, then this wrapper effectively does nothing.

The one constraint that this wrapper has is that the wrapped method's class must have the
:py:class:`objectrocket.client.Client` object embedded in it as the property ``client``. Such
is the design of all current client operations layers.
"""

@functools.wraps(func)
def wrapper(self, *args, **kwargs):
try:
response = func(self, *args, **kwargs)
except errors.AuthFailure:
# Re-raise the exception if the client is not using token authentication.
if not self.client.is_using_tokens:
raise

# Request a new token using the keypair originally given to the client.
self.client._token = self.client.auth.authenticate(self.client.user_key,
self.client.pass_key)
response = func(self, *args, **kwargs)
return response

return wrapper


class Auth(operations.BaseOperationsLayer):
"""Authentication operations.

:param objectrocket.client.Client client_instance: An objectrocket.client.Client instance.
"""

def __init__(self, client_instance):
super(Auth, self).__init__(client_instance=client_instance)

def authenticate(self, user_key, pass_key):
"""Authenticate against the ObjectRocket API.

:param str user_key: The username key used for basic auth.
:param str pass_key: The password key used for basic auth.

:returns: A token used for authentication against token protected resources.
:rtype: str
"""
url = self.url + 'token/'
resp = requests.get(url, auth=(user_key, pass_key),
hooks=dict(response=self.client._verify_auth))

try:
data = resp.json()
token = data['data']
return token
except (ValueError, KeyError) as ex:
raise errors.AuthFailure(str(ex))

@property
def url(self):
"""The base URL for authentication operations."""
return self.client.url + 'auth/'
80 changes: 69 additions & 11 deletions objectrocket/client.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,101 @@
"""Client layer."""
"""ObjectRocket Python client."""
from objectrocket import auth
from objectrocket import instances
from objectrocket import constants
from objectrocket import errors


class Client(object):
"""The base client for ObjectRocket API Python interface.
"""The base client for ObjectRocket's Python interface.

Instantiation of the client will perform API authentication. If API authentication fails,
client instantiation will also fail.

The client will use API tokens by default. If you don't want to use API tokens, and would
rather have the client perform basic authentication using your keypair for each request, set
the ``use_tokens`` parameter to ``False``.

:param str user_key: This is the user key to be used for API authentication.
:param str pass_key: This is the password key to be used for API authentication.
:param bool use_tokens: Instruct the client to use tokens or not.
:param str alternative_url: (optional) An alternative base URL for the client to use. You
shouldn't have to worry about this at all.
"""

def __init__(self, user_key, pass_key, api_url='default'):
def __init__(self, user_key, pass_key, use_tokens=True, **kwargs):
# Client properties.
self._api_url = constants.API_URL_MAP[api_url]
self._url = kwargs.get('alternative_url') or constants.DEFAULT_API_URL
self._user_key = user_key
self._pass_key = pass_key
self._is_using_tokens = use_tokens

# Lazily-created properties.
self._instances = None

# Authenticate.
self._auth = auth.Auth(client_instance=self)
self._token = self._auth.authenticate(user_key=self.user_key, pass_key=self.pass_key)

@property
def auth(self):
"""The authentication operations layer."""
return self._auth

@property
def api_url(self):
"""The base API URL the Client is using."""
return self._api_url
def default_request_kwargs(self):
"""The default request keyword arguments to be passed to the request library."""
default_kwargs = {
'headers': {
'Content-Type': 'application/json',
},
'hooks': {
'response': self._verify_auth,
},
}

# Configue default authentication method based on clients configuration.
if self.is_using_tokens:
default_kwargs['headers']['X-Auth-Token'] = self.token
else:
default_kwargs['auth'] = (self.user_key, self.pass_key)

return default_kwargs

@property
def instances(self):
"""The instance operations layer."""
if self._instances is None:
self._instances = instances.Instances(self)

self._instances = instances.Instances(client_instance=self)
return self._instances

@property
def is_using_tokens(self):
"""A boolean value indicating whether the client is using token authentication or not."""
return self._is_using_tokens

@property
def pass_key(self):
"""The password key currently being used by the Client."""
"""The password key currently being used by this client."""
return self._pass_key

@property
def token(self):
"""The API token this client is currently using."""
return self._token

@property
def url(self):
"""The base URL this client is using."""
return self._url

@property
def user_key(self):
"""The user key currently being used by the Client."""
"""The user key currently being used by this client."""
return self._user_key

def _verify_auth(self, resp, *args, **kwargs):
"""A callback handler to verify that the given response object did not receive a 401."""
if resp.status_code == 401:
raise errors.AuthFailure('Received response code 401 from {} {}. Keypair used: {}:{}'
''.format(resp.request.method, resp.request.path_url,
self.user_key, self.pass_key))
8 changes: 4 additions & 4 deletions objectrocket/constants.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""ObjectRocket Python client constants."""

API_URL_MAP = {
'default': 'http://localhost:5050/v2/', # Point this to the LB when deployed.
'testing': 'http://localhost:5050/v2/',
}
DEFAULT_API_URL = 'https://sjc-api.objectrocket.com/v2/'

MONGODB_SHARDED_INSTANCE = 'mongodb_sharded'
MONGODB_REPLICA_SET_INSTANCE = 'mongodb_replica_set'

TOKUMX_SHARDED_INSTANCE = 'tokumx_sharded'
TOKUMX_REPLICA_SET_INSTANCE = 'tokumx_replica_set'

TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
Loading