Commit c510ec0e authored by Ilias Tsitsimpis's avatar Ilias Tsitsimpis

Merge branch 'feature-astakosclient-auth-url' into develop

parents 7e36eeef b69d4ce9
......@@ -16,6 +16,15 @@ Synnefo-wide
* Integrate Pithos tests in continuous integration.
* Change astakosclient to accept AUTH_URL instead of BASE_URL
ASTAKOS_BASE_URL settings has been removed from Pithos and Cyclades
and has been replaced with ASTAKOS_AUTH_URL. Both Pithos and Cyclades
proxy the Astakos services under ASTAKOS_PROXY_PREFIX path.
ASTAKOS_PROXY_PREFIX by default has a value of '_astakos'.
More specifically, Astakos' identity service is proxied under
'_astakos/identity', Astakos' account service is under '_astakos/account'
and Astakos' ui service is under '_astakos/ui'.
Astakos
-------
......@@ -40,6 +49,9 @@ Astakos
Re-registration with `snf-component-register' affects both the base and
the ui URL.
* Remove API call GET /account/v1.0/authenticate in favor of
POST /identity/v2.0/tokens.
* Management commands:
* Introduced new commands:
* component-show
......
This diff is collapsed.
......@@ -31,8 +31,13 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""
Astakos Client Exceptions
"""
class AstakosClientException(Exception):
"""Base AstakosClientException Class"""
def __init__(self, message='', details='', status=500):
self.message = message
self.details = details
......@@ -43,47 +48,65 @@ class AstakosClientException(Exception):
class BadValue(AstakosClientException):
"""Re-define ValueError Exception under AstakosClientException"""
def __init__(self, details):
"""Re-define ValueError Exception under AstakosClientException"""
message = "ValueError"
super(BadValue, self).__init__(message, details)
class InvalidResponse(AstakosClientException):
"""Return simplejson parse Exception as AstakosClient one"""
def __init__(self, message, details):
"""Return simplejson parse Exception as AstakosClient one"""
super(InvalidResponse, self).__init__(message, details)
class BadRequest(AstakosClientException):
"""BadRequest Exception"""
status = 400
class Unauthorized(AstakosClientException):
"""Unauthorized Exception"""
status = 401
class Forbidden(AstakosClientException):
"""Forbidden Exception"""
status = 403
class NotFound(AstakosClientException):
"""NotFound Exception"""
status = 404
class QuotaLimit(AstakosClientException):
"""QuotaLimit Exception"""
status = 413
class NoUserName(AstakosClientException):
"""No display name for the given uuid"""
def __init__(self, uuid):
"""No display name for the given uuid"""
message = "No display name for the given uuid: %s" % uuid
super(NoUserName, self).__init__(message)
class NoUUID(AstakosClientException):
"""No uuid for the given display name"""
def __init__(self, display_name):
"""No uuid for the given display name"""
message = "No uuid for the given display name: %s" % display_name
super(NoUUID, self).__init__(message)
class NoEndpoints(AstakosClientException):
"""No endpoints found matching the criteria given"""
def __init__(self, ep_name, ep_type, ep_region, ep_version_id):
message = "No endpoints found matching" + \
(", name = %s" % ep_name) if ep_name is not None else "" + \
(", type = %s" % ep_type) if ep_type is not None else "" + \
(", region = %s" % ep_region) \
if ep_region is not None else "" + \
(", version_id = %s" % ep_version_id) \
if ep_version_id is not None else "."
super(NoEndpoints, self).__init__(message)
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import copy
def dict_merge(a, b):
"""
http://www.xormedia.com/recursively-merge-dictionaries-in-python/
"""
if not isinstance(b, dict):
return b
result = copy.deepcopy(a)
for k, v in b.iteritems():
if k in result and isinstance(result[k], dict):
result[k] = dict_merge(result[k], v)
else:
result[k] = copy.deepcopy(v)
return result
def lookup_path(container, path, sep='.', createpath=False):
"""
return (['a','b'],
[container['a'], container['a']['b']],
'c') where path=sep.join(['a','b','c'])
"""
names = path.split(sep)
dirnames = names[:-1]
basename = names[-1]
node = container
name_path = []
node_path = [node]
for name in dirnames:
name_path.append(name)
if name not in node:
if not createpath:
m = "'{0}': path not found".format(sep.join(name_path))
raise KeyError(m)
node[name] = {}
try:
node = node[name]
except TypeError as e:
m = "'{0}': cannot traverse path beyond this node: {1}"
m = m.format(sep.join(name_path), str(e))
raise ValueError(m)
node_path.append(node)
return name_path, node_path, basename
def walk_paths(container):
for name, node in container.iteritems():
if not hasattr(node, 'items'):
yield [name], [node]
else:
for names, nodes in walk_paths(node):
yield [name] + names, [node] + nodes
def list_paths(container, sep='.'):
"""
>>> sorted(list_paths({'a': {'b': {'c': 'd'}}}))
[('a.b.c', 'd')]
>>> sorted(list_paths({'a': {'b': {'c': 'd'}, 'e': 3}}))
[('a.b.c', 'd'), ('a.e', 3)]
>>> sorted(list_paths({'a': {'b': {'c': 'd'}, 'e': {'f': 3}}}))
[('a.b.c', 'd'), ('a.e.f', 3)]
>>> list_paths({})
[]
"""
return [(sep.join(name_path), node_path[-1])
for name_path, node_path in walk_paths(container)]
def del_path(container, path, sep='.', collect=True):
"""
del container['a']['b']['c'] where path=sep.join(['a','b','c'])
>>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c'); d
{}
>>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c', collect=False); d
{'a': {'b': {}}}
>>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c.d')
Traceback (most recent call last):
ValueError: 'a.b.c': cannot traverse path beyond this node:\
'str' object does not support item deletion
"""
name_path, node_path, basename = \
lookup_path(container, path, sep=sep, createpath=False)
lastnode = node_path.pop()
try:
if basename in lastnode:
del lastnode[basename]
except (TypeError, KeyError) as e:
m = "'{0}': cannot traverse path beyond this node: {1}"
m = m.format(sep.join(name_path), str(e))
raise ValueError(m)
if collect:
while node_path and not lastnode:
basename = name_path.pop()
lastnode = node_path.pop()
del lastnode[basename]
def get_path(container, path, sep='.'):
"""
return container['a']['b']['c'] where path=sep.join(['a','b','c'])
>>> get_path({'a': {'b': {'c': 'd'}}}, 'a.b.c.d')
Traceback (most recent call last):
ValueError: 'a.b.c.d': cannot traverse path beyond this node:\
string indices must be integers, not str
>>> get_path({'a': {'b': {'c': 1}}}, 'a.b.c.d')
Traceback (most recent call last):
ValueError: 'a.b.c.d': cannot traverse path beyond this node:\
'int' object is unsubscriptable
>>> get_path({'a': {'b': {'c': 1}}}, 'a.b.c')
1
>>> get_path({'a': {'b': {'c': 1}}}, 'a.b')
{'c': 1}
"""
name_path, node_path, basename = \
lookup_path(container, path, sep=sep, createpath=False)
name_path.append(basename)
node = node_path[-1]
try:
return node[basename]
except TypeError as e:
m = "'{0}': cannot traverse path beyond this node: {1}"
m = m.format(sep.join(name_path), str(e))
raise ValueError(m)
except KeyError as e:
m = "'{0}': path not found: {1}"
m = m.format(sep.join(name_path), str(e))
raise KeyError(m)
def set_path(container, path, value, sep='.',
createpath=False, overwrite=True):
"""
container['a']['b']['c'] = value where path=sep.join(['a','b','c'])
>>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c.d', 1)
Traceback (most recent call last):
ValueError: 'a.b.c.d': cannot traverse path beyond this node:\
'str' object does not support item assignment
>>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.x.d', 1)
Traceback (most recent call last):
KeyError: "'a.b.x': path not found"
>>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.x.d', 1, createpath=True)
>>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c', 1)
>>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c', 1, overwrite=False)
Traceback (most recent call last):
ValueError: will not overwrite path 'a.b.c'
"""
name_path, node_path, basename = \
lookup_path(container, path, sep=sep, createpath=createpath)
name_path.append(basename)
node = node_path[-1]
if basename in node and not overwrite:
m = "will not overwrite path '{0}'".format(path)
raise ValueError(m)
try:
node[basename] = value
except TypeError as e:
m = "'{0}': cannot traverse path beyond this node: {1}"
m = m.format(sep.join(name_path), str(e))
raise ValueError(m)
if __name__ == '__main__':
import doctest
doctest.testmod()
This diff is collapsed.
......@@ -31,6 +31,10 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""
Astakos Client utility module
"""
from httplib import HTTPConnection, HTTPSConnection
from contextlib import closing
......@@ -39,8 +43,10 @@ from objpool.http import PooledHTTPConnection
from astakosclient.errors import AstakosClientException, BadValue
def retry(func):
def retry_dec(func):
"""Class Method Decorator"""
def decorator(self, *args, **kwargs):
"""Retry `self.retry' times if connection fails"""
attemps = 0
while True:
try:
......@@ -64,13 +70,16 @@ def retry(func):
def scheme_to_class(scheme, use_pool, pool_size):
"""Return the appropriate conn class for given scheme"""
def _objpool(netloc):
"""Helper function to return a PooledHTTPConnection object"""
return PooledHTTPConnection(
netloc=netloc, scheme=scheme, size=pool_size)
def _http_connection(netloc):
"""Helper function to return an HTTPConnection object"""
return closing(HTTPConnection(netloc))
def _https_connection(netloc):
"""Helper function to return an HTTPSConnection object"""
return closing(HTTPSConnection(netloc))
if scheme == "http":
......@@ -92,16 +101,22 @@ def parse_request(request, logger):
try:
return simplejson.dumps(request)
except Exception as err:
m = "Cannot parse request \"%s\" with simplejson: %s" \
% (request, str(err))
logger.error(m)
raise BadValue(m)
msg = "Cannot parse request \"%s\" with simplejson: %s" \
% (request, str(err))
logger.error(msg)
raise BadValue(msg)
def check_input(function_name, logger, **kwargs):
"""Check if given arguments are not None"""
for i in kwargs:
if kwargs[i] is None:
m = "in " + function_name + ": " + str(i) + " parameter not given"
logger.error(m)
raise BadValue(m)
msg = "in " + function_name + ": " + \
str(i) + " parameter not given"
logger.error(msg)
raise BadValue(msg)
def join_urls(url_a, url_b):
"""Join_urls from synnefo.lib"""
return url_a.rstrip("/") + "/" + url_b.lstrip("/")
This diff is collapsed.
......@@ -176,6 +176,7 @@ setup(
zip_safe=False,
install_requires=INSTALL_REQUIRES,
tests_require=['mock'],
entry_points={},
)
......@@ -64,7 +64,7 @@ cmd_options = --nofailfast --no-ipv6 --action-timeout=240
[Unit Tests]
component = astakos cyclades pithos
component = astakos cyclades pithos astakosclient
[Repository]
......
......@@ -64,7 +64,7 @@ cmd_options = --nofailfast --no-ipv6 --action-timeout=240
[Unit Tests]
component = astakos cyclades pithos
component = astakos cyclades pithos astakosclient
[Repository]
......
......@@ -4,15 +4,19 @@ set -e
SNF_MANAGE=$(which snf-manage) ||
{ echo "Cannot find snf-manage in $PATH" 1>&2; exit 1; }
runtest () {
runTest () {
TEST="$SNF_MANAGE test $* --traceback --noinput --settings=synnefo.settings.test"
runCoverage "$TEST"
}
runCoverage () {
if coverage >/dev/null 2>&1; then
coverage run $TEST
coverage run $1
coverage report --include=snf-*
else
echo "WARNING: Cannot find coverage in path, skipping coverage tests" 1>&2
$TEST
$1
fi
}
......@@ -24,17 +28,20 @@ PITHOS_APPS="api"
TEST_COMPONENTS="$@"
if [ -z "$TEST_COMPONENTS" ]; then
TEST_COMPONENTS="astakos cyclades pithos"
TEST_COMPONENTS="astakos cyclades pithos astakosclient"
fi
for component in $TEST_COMPONENTS; do
if [ "$component" = "astakos" ]; then
runtest $ASTAKOS_APPS
runTest $ASTAKOS_APPS
elif [ "$component" = "cyclades" ]; then
export SYNNEFO_EXCLUDE_PACKAGES="snf-pithos-app"
runtest $CYCLADES_APPS
runTest $CYCLADES_APPS
elif [ "$component" = "pithos" ]; then
export SYNNEFO_EXCLUDE_PACKAGES="snf-cyclades-app"
runtest $PITHOS_APPS
runTest $PITHOS_APPS
elif [ "$component" = "astakosclient" ]; then
TEST="nosetests astakosclient"
runCoverage "$TEST"
fi
done
......@@ -303,6 +303,7 @@ class SynnefoCI(object):
self.logger.debug("Setup apt. Install x2goserver and firefox")
cmd = """
echo 'APT::Install-Suggests "false";' >> /etc/apt/apt.conf
echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
apt-get update
apt-get install curl --yes --force-yes
echo -e "\n\n{0}" >> /etc/apt/sources.list
......@@ -856,6 +857,7 @@ class SynnefoCI(object):
cmd = """
pip install -U mock
pip install -U factory_boy
pip install -U nose
"""
_run(cmd, False)
......
......@@ -20,6 +20,7 @@ Document Revisions
========================= ================================
Revision Description
========================= ================================
0.15 (October 29, 2013) Remove GET /authenticate in favor of POST /tokens
0.14 (June 03, 2013) Remove endpoint listing
0.14 (May 28, 2013) Extend token api with authenticate call
0.14 (May 23, 2013) Extend api to list endpoints
......@@ -79,71 +80,11 @@ Example reply if request user is authenticated:
User API Operations
--------------------
The operations described in this chapter allow users to authenticate themselves, send feedback and get user uuid/displayname mappings.
The operations described in this chapter allow users to send feedback and
get user uuid/displayname mappings.
All the operations require a valid user token.
.. _authenticate-api-label:
Authenticate
^^^^^^^^^^^^
Authenticate API requests require a token. An application that wishes to connect to Astakos, but does not have a token, should redirect the user to ``/login``. (see :ref:`authentication-label`)
============================== ========= ==================
Uri Method Description
============================== ========= ==================
``/account/v1.0/authenticate`` GET Authenticate user using token
============================== ========= ==================
|
==================== ===========================
Request Header Name Value
==================== ===========================
X-Auth-Token User authentication token
==================== ===========================
Extended information on the user serialized in the json format will be returned:
=========================== ============================
Name Description
=========================== ============================
displayname User displayname
uuid User unique identifier
email List with user emails
name User full name
auth_token_created Token creation date
auth_token_expires Token expiration date
=========================== ============================
Example reply:
::
{"id": "12",
"displayname": "user@example.com",
"uuid": "a9dc21d2-bcb2-4104-9a9e-402b7c70d6d8",
"email": "[user@example.com]",
"name": "Firstname Lastname",
"auth_token_created": "Wed, 30 May 2012 10:03:37 GMT",
"auth_token_expires": "Fri, 29 Jun 2012 10:03:37 GMT"}
|
=========================== =====================
Return Code Description
=========================== =====================
204 (No Content) The request succeeded
400 (Bad Request) Method not allowed or no user found
401 (Unauthorized) Missing token or inactive user or penging approval terms
500 (Internal Server Error) The request cannot be completed because of an internal error
=========================== =====================
.. warning:: The service is also available under ``/ui/authenticate``.
It will be removed in the next version.
Send feedback
^^^^^^^^^^^^^
......@@ -310,12 +251,12 @@ Tokens API Operations
Authenticate
^^^^^^^^^^^^
Fallback call which receives the user token or the user uuid/token pair and
returns back the token as well as information about the token holder and the
services he/she can access.
If not request body is provided (the request content length is missing or
equals to 0) the response contains only non authentication protected
information (the service catalog).
This call takes the user token or the user uuid/token pair, authenticates
the user and returns information about the token, its holder as well as
a list of services the user can access.
If no request body is provided (the request content length is missing or
equals to 0), the call operates in public mode, attempts no authentication
and returns only the service catalog.
========================================= ========= ==================
Uri Method Description
......
......@@ -1042,7 +1042,7 @@ this options:
.. code-block:: console
ASTAKOS_BASE_URL = 'https://node1.example.com/astakos'
ASTAKOS_AUTH_URL = 'https://node1.example.com/astakos/identity/v2.0'
PITHOS_BASE_URL = 'https://node2.example.com/pithos'
PITHOS_BACKEND_DB_CONNECTION = 'postgresql://synnefo:example_passw0rd@node1.example.com:5432/snf_pithos'
......@@ -1050,9 +1050,6 @@ this options:
PITHOS_SERVICE_TOKEN = 'pithos_service_token22w'
# Set to False if astakos & pithos are on the same host
PITHOS_PROXY_USER_SERVICES = True
The ``PITHOS_BACKEND_DB_CONNECTION`` option tells to the Pithos app where to
find the Pithos backend database. Above we tell Pithos that its database is
......@@ -1065,7 +1062,7 @@ the Pithos backend data. Above we tell Pithos to store its data under
``/srv/pithos/data``, which is visible by both nodes. We have already setup this
directory at node1's "Pithos data directory setup" section.
The ``ASTAKOS_BASE_URL`` option informs the Pithos app where Astakos is.
The ``ASTAKOS_AUTH_URL`` option informs the Pithos app where Astakos is.
The Astakos service is used for user management (authentication, quotas, etc.)
The ``PITHOS_BASE_URL`` setting must point to the top-level Pithos URL.
......@@ -1856,17 +1853,14 @@ Edit ``/etc/synnefo/20-snf-cyclades-app-api.conf``:
.. code-block:: console
CYCLADES_BASE_URL = 'https://node1.example.com/cyclades'
ASTAKOS_BASE_URL = 'https://node1.example.com/astakos'
# Set to False if astakos & cyclades are on the same host
CYCLADES_PROXY_USER_SERVICES = False
ASTAKOS_AUTH_URL = 'https://node1.example.com/astakos/identity/v2.0'