Commit 005d6796 authored by Giorgos Korfiatis's avatar Giorgos Korfiatis Committed by Ilias Tsitsimpis

astakosclient: Separate public/private POST /tokens

Introduce function authenticate(), which performs POST /tokens in
private mode, i.e. giving a token to be checked for authentication.
Function get_endpoints() performs the call in public mode, in order
to retrieve the endpoints.

Initialize endpoints lazily: when a URL is missing, get_endpoints()
is triggered.
parent d4b9bdba
......@@ -127,61 +127,99 @@ class AstakosClient(object):
self.auth_prefix = parsed_auth_url.path
self.api_tokens = join_urls(self.auth_prefix, "tokens")
# ------------------------------
# API urls under account_url
# Get account_url from get_endpoints
# get_endpoints needs self.api_tokens
endpoints = self.get_endpoints(non_authentication=True)
account_service_catalog = parse_endpoints(
def _fill_endpoints(self, endpoints):
astakos_service_catalog = parse_endpoints(
endpoints, ep_name="astakos_account", ep_version_id="v1.0")
self.account_url = \
account_service_catalog[0]['endpoints'][0]['publicURL']
parsed_account_url = urlparse.urlparse(self.account_url)
self.account_prefix = parsed_account_url.path
self.logger.debug("Got account_prefix \"%s\"" % self.account_prefix)
self.api_authenticate = join_urls(
self.account_prefix, "authenticate")
self.api_usercatalogs = join_urls(
self.account_prefix, "user_catalogs")
self.api_service_usercatalogs = join_urls(
self.account_prefix, "service/user_catalogs")
self.api_resources = join_urls(
self.account_prefix, "resources")
self.api_quotas = join_urls(
self.account_prefix, "quotas")
self.api_service_quotas = join_urls(
self.account_prefix, "service_quotas")
self.api_commissions = join_urls(
self.account_prefix, "commissions")
self.api_commissions_action = join_urls(
self.api_commissions, "action")
self.api_feedback = join_urls(
self.account_prefix, "feedback")
self.api_projects = join_urls(
self.account_prefix, "projects")
self.api_applications = join_urls(
self.api_projects, "apps")
self.api_memberships = join_urls(
self.api_projects, "memberships")
# ------------------------------
# API urls under ui_url
# Get ui url from get_endpoints
# get_endpoints needs self.api_tokens
ui_service_catalog = parse_endpoints(
endpoints, ep_name="astakos_account", ep_version_id="v1.0")
parsed_ui_url = urlparse.urlparse(
ui_service_catalog[0]['endpoints'][0]['SNF:uiURL'])
self.ui_url = \
ui_service_catalog[0]['endpoints'][0]['SNF:uiURL']
parsed_ui_url = urlparse.urlparse(self.ui_url)
self._account_url = \
astakos_service_catalog[0]['endpoints'][0]['publicURL']
parsed_account_url = urlparse.urlparse(self._account_url)
self._account_prefix = parsed_account_url.path
self.logger.debug("Got account_prefix \"%s\"" % self._account_prefix)
self._ui_url = \
astakos_service_catalog[0]['endpoints'][0]['SNF:uiURL']
parsed_ui_url = urlparse.urlparse(self._ui_url)
self._ui_prefix = parsed_ui_url.path
self.logger.debug("Got ui_prefix \"%s\"" % self._ui_prefix)
def _get_value(self, s):
assert s in ['_account_url', '_account_prefix',
'_ui_url', '_ui_prefix']
try:
return getattr(self, s)
except AttributeError:
self.get_endpoints()
return getattr(self, s)
@property
def account_url(self):
return self._get_value('_account_url')
@property
def account_prefix(self):
return self._get_value('_account_prefix')
@property
def ui_url(self):
return self._get_value('_ui_url')
@property
def ui_prefix(self):
return self._get_value('_ui_prefix')
@property
def api_authenticate(self):
return join_urls(self.account_prefix, "authenticate")
@property
def api_usercatalogs(self):
return join_urls(self.account_prefix, "user_catalogs")
@property
def api_service_usercatalogs(self):
return join_urls(self.account_prefix, "service/user_catalogs")
@property
def api_resources(self):
return join_urls(self.account_prefix, "resources")
@property
def api_quotas(self):
return join_urls(self.account_prefix, "quotas")
@property
def api_service_quotas(self):
return join_urls(self.account_prefix, "service_quotas")
self.ui_prefix = parsed_ui_url.path
self.logger.debug("Got ui_prefix \"%s\"" % self.ui_prefix)
@property
def api_commissions(self):
return join_urls(self.account_prefix, "commissions")
self.api_getservices = join_urls(self.ui_prefix, "get_services")
@property
def api_commissions_action(self):
return join_urls(self.api_commissions, "action")
@property
def api_feedback(self):
return join_urls(self.account_prefix, "feedback")
@property
def api_projects(self):
return join_urls(self.account_prefix, "projects")
@property
def api_applications(self):
return join_urls(self.api_projects, "apps")
@property
def api_memberships(self):
return join_urls(self.api_projects, "memberships")
@property
def api_getservices(self):
return join_urls(self.ui_prefix, "get_services")
# ----------------------------------
@retry_dec
......@@ -393,40 +431,49 @@ class AstakosClient(object):
self._call_astakos(self.api_feedback, headers=None,
body=req_body, method="POST")
# ----------------------------------
# do a POST to ``API_TOKENS``
def get_endpoints(self, tenant_name=None, non_authentication=False):
# -----------------------------------------
# do a POST to ``API_TOKENS`` with no token
def get_endpoints(self):
""" Get services' endpoints
In case of error raise an AstakosClientException.
"""
req_headers = {'content-type': 'application/json'}
req_body = None
r = self._call_astakos(self.api_tokens, headers=req_headers,
body=req_body, method="POST",
log_body=False)
self._fill_endpoints(r)
return r
# --------------------------------------
# do a POST to ``API_TOKENS`` with a token
def authenticate(self, tenant_name=None):
""" Authenticate and get services' endpoints
Keyword arguments:
tenant_name -- user's uniq id (optional)
non_authentication -- get only non authentication protected info
It returns back the token as well as information about the token
holder and the services he/she can acess (in json format).
holder and the services he/she can access (in json format).
The tenant_name is optional and if it is given it must match the
user's uuid.
In case on of the `name', `type', `region', `version_id' parameters
is given, return only the endpoints that match all of these criteria.
If no match is found then raise NoEndpoints exception.
In case of error raise an AstakosClientException.
"""
req_headers = {'content-type': 'application/json'}
if non_authentication:
req_body = None
else:
body = {'auth': {'token': {'id': self.token}}}
if tenant_name is not None:
body['auth']['tenantName'] = tenant_name
req_body = parse_request(body, self.logger)
return self._call_astakos(self.api_tokens, headers=req_headers,
body=req_body, method="POST",
log_body=False)
body = {'auth': {'token': {'id': self.token}}}
if tenant_name is not None:
body['auth']['tenantName'] = tenant_name
req_body = parse_request(body, self.logger)
r = self._call_astakos(self.api_tokens, headers=req_headers,
body=req_body, method="POST",
log_body=False)
self._fill_endpoints(r)
return r
# ----------------------------------
# do a GET to ``API_QUOTAS``
......@@ -885,7 +932,7 @@ def parse_endpoints(endpoints, ep_name=None, ep_type=None,
# --------------------------------------------------------------------
# Private functions
# We want _doRequest to be a distinct function
# We want _do_request to be a distinct function
# so that we can replace it during unit tests.
def _do_request(conn, method, url, **kwargs):
"""The actual request. This function can easily be mocked"""
......
......@@ -42,10 +42,7 @@ the astakos client library
import re
import sys
import socket
import simplejson
from mock import patch
from contextlib import contextmanager
import astakosclient
from astakosclient import AstakosClient
......@@ -68,7 +65,7 @@ except ImportError:
auth_url = "https://example.org/identity/v2.0"
account_prefix = "/account_prefix"
ui_prefix = "/ui_prefix"
api_authenticate = join_urls(account_prefix, "authenticate")
api_tokens = "/identity/v2.0/tokens"
api_usercatalogs = join_urls(account_prefix, "user_catalogs")
api_resources = join_urls(account_prefix, "resources")
api_quotas = join_urls(account_prefix, "quotas")
......@@ -76,12 +73,21 @@ api_commissions = join_urls(account_prefix, "commissions")
# --------------------------------------
# Local users
token_1 = "skzleaFlBl+fasFdaf24sx"
user_1 = \
{"username": "user1@example.com",
"name": "Example User One",
"email": ["user1@example.com"],
"uuid": "73917abc-abcd-477e-a1f1-1763abcdefab"}
token = {
'id': "skzleaFlBl+fasFdaf24sx",
'tenant': {
'id': "73917abc-abcd-477e-a1f1-1763abcdefab",
'name': "Example User One",
},
}
user = {
'id': "73917abc-abcd-477e-a1f1-1763abcdefab",
'name': "Example User One",
'roles': [{u'id': 1, u'name': u'default'},
{u'id': 5, u'name': u'academic-login-users'}],
'roles_links': []
}
resources = {
"cyclades.ram": {
......@@ -103,6 +109,10 @@ endpoints = {
}
}
endpoints_with_info = dict(endpoints)
endpoints_with_info['access']['token'] = dict(token)
endpoints_with_info['access']['user'] = dict(user)
quotas = {
"system": {
"cyclades.ram": {
......@@ -197,11 +207,6 @@ resolve_commissions_rep = {
# ----------------------------
# These functions will be used as mocked requests
def _request_offline(conn, method, url, **kwargs):
"""This request behaves as we were offline"""
raise socket.gaierror
def _request_status_302(conn, method, url, **kwargs):
"""This request returns 302"""
message = "FOUND"
......@@ -242,10 +247,10 @@ def _request_status_400(conn, method, url, **kwargs):
return (message, data, status)
def _request_ok(conn, method, url, **kwargs):
def _mock_request(conn, method, url, **kwargs):
"""This request behaves like original Astakos does"""
if api_authenticate == url:
return _req_authenticate(conn, method, url, **kwargs)
if api_tokens == url:
return _req_tokens(conn, method, url, **kwargs)
elif api_usercatalogs == url:
return _req_catalogs(conn, method, url, **kwargs)
elif api_resources == url:
......@@ -258,35 +263,38 @@ def _request_ok(conn, method, url, **kwargs):
return _request_status_404(conn, method, url, **kwargs)
def _req_authenticate(conn, method, url, **kwargs):
"""Check if user exists and return his profile"""
global user_1, token_1
def _req_tokens(conn, method, url, **kwargs):
"""Return endpoints"""
global token, user, endpoints
# Check input
if conn.__class__.__name__ != "HTTPSConnection":
return _request_status_302(conn, method, url, **kwargs)
if method != "GET":
if method != "POST":
return _request_status_400(conn, method, url, **kwargs)
token = kwargs['headers'].get('X-Auth-Token')
if token == token_1:
user = dict(user_1)
return ("", simplejson.dumps(user), 200)
else:
# No user found
req_token = kwargs['headers'].get('X-Auth-Token')
if req_token != token['id']:
return _request_status_401(conn, method, url, **kwargs)
if 'body' in kwargs:
# Return endpoints with authenticate info
return ("", simplejson.dumps(endpoints_with_info), 200)
else:
# Return endpoints without authenticate info
return ("", simplejson.dumps(endpoints), 200)
def _req_catalogs(conn, method, url, **kwargs):
"""Return user catalogs"""
global token_1, token_2, user_1, user_2
global token, user
# Check input
if conn.__class__.__name__ != "HTTPSConnection":
return _request_status_302(conn, method, url, **kwargs)
if method != "POST":
return _request_status_400(conn, method, url, **kwargs)
token = kwargs['headers'].get('X-Auth-Token')
if token != token_1:
req_token = kwargs['headers'].get('X-Auth-Token')
if req_token != token['id']:
return _request_status_401(conn, method, url, **kwargs)
# Return
......@@ -295,15 +303,15 @@ def _req_catalogs(conn, method, url, **kwargs):
# Return uuid_catalog
uuids = body['uuids']
catalogs = {}
if user_1['uuid'] in uuids:
catalogs[user_1['uuid']] = user_1['username']
if user['id'] in uuids:
catalogs[user['id']] = user['name']
return_catalog = {"displayname_catalog": {}, "uuid_catalog": catalogs}
elif 'displaynames' in body:
# Return displayname_catalog
names = body['displaynames']
catalogs = {}
if user_1['username'] in names:
catalogs[user_1['username']] = user_1['uuid']
if user['name'] in names:
catalogs[user['name']] = user['id']
return_catalog = {"displayname_catalog": catalogs, "uuid_catalog": {}}
else:
return_catalog = {"displayname_catalog": {}, "uuid_catalog": {}}
......@@ -326,15 +334,15 @@ def _req_resources(conn, method, url, **kwargs):
def _req_quotas(conn, method, url, **kwargs):
"""Return quotas for user_1"""
global token_1, quotas
global token, quotas
# Check input
if conn.__class__.__name__ != "HTTPSConnection":
return _request_status_302(conn, method, url, **kwargs)
if method != "GET":
return _request_status_400(conn, method, url, **kwargs)
token = kwargs['headers'].get('X-Auth-Token')
if token != token_1:
req_token = kwargs['headers'].get('X-Auth-Token')
if req_token != token['id']:
return _request_status_401(conn, method, url, **kwargs)
# Return
......@@ -343,14 +351,14 @@ def _req_quotas(conn, method, url, **kwargs):
def _req_commission(conn, method, url, **kwargs):
"""Perform a commission for user_1"""
global token_1, pending_commissions, \
global token, pending_commissions, \
commission_successful_response, commission_failure_response
# Check input
if conn.__class__.__name__ != "HTTPSConnection":
return _request_status_302(conn, method, url, **kwargs)
token = kwargs['headers'].get('X-Auth-Token')
if token != token_1:
req_token = kwargs['headers'].get('X-Auth-Token')
if req_token != token['id']:
return _request_status_401(conn, method, url, **kwargs)
if method == "POST":
......@@ -399,65 +407,15 @@ def _req_commission(conn, method, url, **kwargs):
return _request_status_400(conn, method, url, **kwargs)
# ----------------------------
# Mock the actual _doRequest
def _mock_request(new_requests):
"""Mock the actual request
Given a list of requests to use (in rotation),
replace the original _doRequest function with
a new one
"""
def _mock(conn, method, url, **kwargs):
# Get first request
request = _mock.requests[0]
# Rotate requests
_mock.requests = _mock.requests[1:] + _mock.requests[:1]
# Use first request
return request(conn, method, url, **kwargs)
_mock.requests = new_requests
# Replace `_doRequest' with our `_mock'
astakosclient._do_request = _mock
# --------------------------------------
# Mock the get_endpoints method
@contextmanager
def patch_astakosclient(new_requests):
_mock_request(new_requests)
with patch('astakosclient.AstakosClient.get_endpoints') as patcher:
patcher.return_value = endpoints
yield
# --------------------------------------------------------------------
# The actual tests
class TestCallAstakos(unittest.TestCase):
"""Test cases for function _callAstakos"""
# ----------------------------------
# Test the response we get if we don't have internet access
def _offline(self, pool):
global token_1, auth_url
_mock_request([_request_offline])
try:
client = AstakosClient(token_1, auth_url, use_pool=pool)
client._call_astakos("offline")
except AstakosClientException:
pass
else:
self.fail("Should have raised AstakosClientException")
def test_offline(self):
"""Test _offline without pool"""
self._offline(False)
def test_offline_pool(self):
"""Test _offline using pool"""
self._offline(True)
# Patch astakosclient's _do_request function
def setUp(self):
astakosclient._do_request = _mock_request
# ----------------------------------
# Test the response we get if we send invalid token
......@@ -465,9 +423,8 @@ class TestCallAstakos(unittest.TestCase):
global auth_url
token = "skaksaFlBl+fasFdaf24sx"
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token, auth_url, use_pool=pool)
client.get_user_info()
client = AstakosClient(token, auth_url, use_pool=pool)
client.authenticate()
except Unauthorized:
pass
except Exception:
......@@ -486,11 +443,10 @@ class TestCallAstakos(unittest.TestCase):
# ----------------------------------
# Test the response we get if we send invalid url
def _invalid_url(self, pool):
global token_1, auth_url
global token, auth_url
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token_1, auth_url, use_pool=pool)
client._call_astakos("/astakos/api/misspelled")
client = AstakosClient(token['id'], auth_url, use_pool=pool)
client._call_astakos("/astakos/api/misspelled")
except NotFound:
pass
except Exception, e:
......@@ -509,12 +465,11 @@ class TestCallAstakos(unittest.TestCase):
# ----------------------------------
# Test the response we get if we use an unsupported scheme
def _unsupported_scheme(self, pool):
global token_1, auth_url
global token, auth_url
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token_1, "ftp://example.com",
use_pool=pool)
client.get_user_info()
client = AstakosClient(
token['id'], "ftp://example.com", use_pool=pool)
client.authenticate()
except BadValue:
pass
except Exception:
......@@ -533,12 +488,11 @@ class TestCallAstakos(unittest.TestCase):
# ----------------------------------
# Test the response we get if we use http instead of https
def _http_scheme(self, pool):
global token_1
global token
http_auth_url = "http://example.org/identity/v2.0"
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token_1, http_auth_url, use_pool=pool)
client.get_user_info()
client = AstakosClient(token['id'], http_auth_url, use_pool=pool)
client.authenticate()
except AstakosClientException as err:
if err.status != 302:
self.fail("Should have returned 302 (Found)")
......@@ -554,13 +508,12 @@ class TestCallAstakos(unittest.TestCase):
self._http_scheme(True)
# ----------------------------------
# Test the response we get if we use authenticate with POST
def _post_authenticate(self, pool):
global token_1, auth_url
# Test the response we get if we use authenticate with GET
def _get_authenticate(self, pool):
global token, auth_url
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token_1, auth_url, use_pool=pool)
client._call_astakos(api_authenticate, method="POST")
client = AstakosClient(token['id'], auth_url, use_pool=pool)
client._call_astakos(api_tokens, method="GET")
except BadRequest:
pass
except Exception:
......@@ -568,22 +521,21 @@ class TestCallAstakos(unittest.TestCase):
else:
self.fail("Should have returned 400 (Method not allowed)")
def test_post_authenticate(self):
"""Test _post_authenticate without pool"""
self._post_authenticate(False)
def test_get_authenticate(self):
"""Test _get_authenticate without pool"""
self._get_authenticate(False)
def test_post_authenticate_pool(self):
"""Test _post_authenticate using pool"""
self._post_authenticate(True)
def test_get_authenticate_pool(self):
"""Test _get_authenticate using pool"""
self._get_authenticate(True)
# ----------------------------------
# Test the response if we request user_catalogs with GET
def _get_user_catalogs(self, pool):
global token_1, auth_url, api_usercatalogs
global token, auth_url, api_usercatalogs
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token_1, auth_url, use_pool=pool)
client._call_astakos(api_usercatalogs)
client = AstakosClient(token['id'], auth_url, use_pool=pool)
client._call_astakos(api_usercatalogs)
except BadRequest:
pass
except Exception:
......@@ -603,19 +555,9 @@ class TestCallAstakos(unittest.TestCase):
class TestAuthenticate(unittest.TestCase):
"""Test cases for function getUserInfo"""
# ----------------------------------
# Test the response we get if we don't have internet access
def test_offline(self):
"""Test offline after 3 retries"""
global token_1, auth_url
try:
with patch_astakosclient([_request_offline]):
client = AstakosClient(token_1, auth_url, retry=3)
client.get_user_info()
except AstakosClientException:
pass
else:
self.fail("Should have raised AstakosClientException exception")
# Patch astakosclient's _do_request function
def setUp(self):
astakosclient._do_request = _mock_request
# ----------------------------------
# Test the response we get for invalid token
......@@ -623,9 +565,8 @@ class TestAuthenticate(unittest.TestCase):
global auth_url
token = "skaksaFlBl+fasFdaf24sx"
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token, auth_url, use_pool=pool)
client.get_user_info()
client = AstakosClient(token, auth_url, use_pool=pool)
client.authenticate()
except Unauthorized:
pass
except Exception:
......@@ -641,58 +582,42 @@ class TestAuthenticate(unittest.TestCase):
"""Test _invalid_token using pool"""
self._invalid_token(True)
#- ---------------------------------
# ----------------------------------
# Test response for user
def _auth_user(self, token, user_info, pool):
global auth_url
def _auth_user(self, pool):
global token, endpoints_with_info, auth_url
try:
with patch_astakosclient([_request_ok]):
client = AstakosClient(token, auth_url, use_pool=pool)
auth_info = client.get_user_info()
client = AstakosClient(token['id'], auth_url, use_pool=pool)
auth_info = client.authenticate()
except:
self.fail("Shouldn't raise an Exception")
self.assertEqual(user_info, auth_info)
self.assertEqual(endpoints_with_info, auth_info)
def test_auth_user(self):
"""Test _auth_user without pool"""
global token_1, user_1
user_info = dict(user_1)
self._auth_user(token_1, user_info, False)
self._auth_user(False)
def test_auth_user_pool(self):
"""Test _auth_user for User 1 using pool, with usage"""
global token_1, user_1
self._auth_user(token_1, user_1, True)
# ----------------------------------
# Test retry functionality
def test_offline_retry(self):
"""Test retry functionality for getUserInfo"""
global token_1, user_1, auth_url
_mock_request([_request_offline, _request_offline, _request_ok])
try: