Commit 2de22a35 authored by Sofia Papagiannaki's avatar Sofia Papagiannaki

Merge branch 'feature-pithos-file-serve-view' into develop

parents 2a22ad77 8c534756
......@@ -29,6 +29,8 @@ Synnefo-wide
notifactions to users listed in 'ADMINS' setting about unhandled exceptions
in the code.
* Extend astakosclient to request and validate OAuth 2.0 access tokens
Astakos
-------
......@@ -77,11 +79,19 @@ Astakos
'ASTAKOS_ADMIN_STATS_PERMITTED_GROUPS' setting. Statistics are also availble
from 'snf-manage stats-astakos' management command.
* Implement OAuth 2.0 Authorization Code Grant
Add API calls for authorization code and access token generation
* Add API call for validating OAuth 2.0 access tokens
* Management commands:
* Introduced new commands:
* component-show
* quota-list (replacing quota, supports various filters)
* quota-verify (replacing quota)
* oauth2-client-add (register OAuth 2.0 client)
* oauth2-client-list (list registered oauth 2.0 clients)
* oauth2-client-remove (remove OAuth 2.0 client)
* Changed commands:
* component-add got options --base-url and --ui-url
* resource-modify --limit became --default-quota
......@@ -189,6 +199,19 @@ Pithos
* Introduced new command:
* file-show
* Change view authentication
The pithos views do not use the cookie information for user authentication.
They request (from Astakos) and use a short-term access token for a
specific resource.
* Remove PITHOS_ASTAKOS_COOKIE_NAME setting, since it is no longer useful
* Add PITHOS_OAUTH2_CLIENT_CREDENTIALS setting to authenticate the views with
astakos during the resource access token generation procedure
* Add PITHOS_SERVE_API_DOMAIN setting to restrict file serving endpoints to a
specific host
* Refactor metadata schema (table attributes) in Pithos DB to speedup current
objects by domain attribute. This is used by Plankton for listing VM images.
......
......@@ -39,6 +39,7 @@ import logging
import urlparse
import urllib
import hashlib
from base64 import b64encode
from copy import copy
import simplejson
......@@ -144,9 +145,17 @@ class AstakosClient(object):
self._ui_prefix = parsed_ui_url.path
self.logger.debug("Got ui_prefix \"%s\"" % self._ui_prefix)
oauth2_service_catalog = parse_endpoints(endpoints,
ep_name="astakos_oauth2")
self._oauth2_url = \
oauth2_service_catalog[0]['endpoints'][0]['publicURL']
parsed_oauth2_url = urlparse.urlparse(self._oauth2_url)
self._oauth2_prefix = parsed_oauth2_url.path
def _get_value(self, s):
assert s in ['_account_url', '_account_prefix',
'_ui_url', '_ui_prefix']
'_ui_url', '_ui_prefix',
'_oauth2_url', '_oauth2_prefix']
try:
return getattr(self, s)
except AttributeError:
......@@ -169,6 +178,14 @@ class AstakosClient(object):
def ui_prefix(self):
return self._get_value('_ui_prefix')
@property
def oauth2_url(self):
return self._get_value('_oauth2_url')
@property
def oauth2_prefix(self):
return self._get_value('_oauth2_prefix')
@property
def api_usercatalogs(self):
return join_urls(self.account_prefix, "user_catalogs")
......@@ -217,6 +234,14 @@ class AstakosClient(object):
def api_getservices(self):
return join_urls(self.ui_prefix, "get_services")
@property
def api_oauth2_auth(self):
return join_urls(self.oauth2_prefix, "auth")
@property
def api_oauth2_token(self):
return join_urls(self.oauth2_prefix, "token")
# ----------------------------------
@retry_dec
def _call_astakos(self, request_path, headers=None,
......@@ -473,6 +498,29 @@ class AstakosClient(object):
self._fill_endpoints(r)
return r
# --------------------------------------
# do a GET to ``API_TOKENS`` with a token
def validate_token(self, token_id, belongsTo=None):
""" Validate a temporary access token (oath2)
Keyword arguments:
belongsTo -- confirm that token belongs to tenant
It returns back the token as well as information about the token
holder.
The belongsTo is optional and if it is given it must be inside the
token's scope.
In case of error raise an AstakosClientException.
"""
path = join_urls(self.api_tokens, str(token_id))
if belongsTo is not None:
params = {'belongsTo': belongsTo}
path = '%s?%s' % (path, urllib.urlencode(params))
return self._call_astakos(path, method="GET", log_body=False)
# ----------------------------------
# do a GET to ``API_QUOTAS``
def get_quotas(self):
......@@ -877,6 +925,18 @@ class AstakosClient(object):
return self._call_astakos(self.api_memberships, headers=req_headers,
body=req_body, method="POST")
# --------------------------------
# do a POST to ``API_OAUTH2_TOKEN``
def get_token(self, grant_type, client_id, client_secret, **body_params):
headers = {'content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic %s' % b64encode('%s:%s' %
(client_id,
client_secret))}
body_params['grant_type'] = grant_type
body = urllib.urlencode(body_params)
return self._call_astakos(self.api_oauth2_token, headers=headers,
body=body, method="POST")
# --------------------------------------------------------------------
# parse endpoints
......
......@@ -1504,6 +1504,9 @@ group-list List available groups
user-list List users
user-modify Modify user
user-show Show user details
oauth2-client-add Create an oauth2 client
oauth2-client-list List oauth2 clients
oauth2-client-remove Remove an oauth2 client along with its registered redirect urls
============================ ===========================
Pithos snf-manage commands
......
......@@ -20,6 +20,7 @@ Document Revisions
========================= ================================
Revision Description
========================= ================================
0.15 (December 02, 2013) Extent token api with validate token call
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
......@@ -428,3 +429,51 @@ Return Code Description
401 (Unauthorized) Invalid token or invalid creadentials or tenantName does not comply with the provided token
500 (Internal Server Error) The request cannot be completed because of an internal error
=========================== =====================
Validate token
^^^^^^^^^^^^^^
This calls validates an access token and confirms that it belongs to a
specified scope.
========================================= ========= ==================
Uri Method Description
========================================= ========= ==================
``/identity/v2.0/tokens/<token_id>`` GET Validates an access token and confirms that it belongs to a specified scope.
========================================= ========= ==================
|
====================== =========================
Request Parameter Name Value
====================== =========================
belongsTo Validates that a access token has the specified scope.
The belongsTo parameter is optional.
====================== =========================
Example response
::
{"access": {
"token": {
"expires": "2013-12-02T15:57:34.300266+00:00",
"id": "2YotnFZFEjr1zCsicMWpAA",
"tenant": {
"id": "c18088be-16b1-4263-8180-043c54e22903",
"name": "Firstname Lastname"
}
},
"user": {
"roles_links": [],
"id": "c18088be-16b1-4263-8180-043c54e22903",
"roles": [{"id": 1, "name": "default"}],
"name": "Firstname Lastname"}}}
|
=========================== =====================
Return Code Description
=========================== =====================
404 Unknown or expired access token or the access token does not belong to the specified scope
=========================== =====================
Serve untrusted user content
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We want to serve untrusted user content in a domain which does not have access
to sensitive information. The information used by pithos view is set by astakos
in the cookie after a successful user authentication login. Starting from
synnefo version 0.15, the pithos view will be deployed in a domain outside the
astakos cookie domain. The current document describes how the pithos view can
grant access to the protected pithos resources.
The proposed scheme follows the guidelines of the OAuth 2.0 authentication
framework as described in http://tools.ietf.org/html/rfc6749/.
Briefly the pithos view requests a short-term access token for a specific
resource from astakos. Before requesting the access token, the view obtains
an authorization grant (authorization code) from astakos, which is then
presented by the view during the request for the access token.
Pithos view registration to astakos
===================================
The pithos view has to authenticate itself with astakos since the latter has to
prevent serving requests by unknown/unauthorized clients.
Each oauth client is identified by a client identifier (client_id). Moreover,
the confidential clients are authenticated via a password (client_secret).
Then, each client has to declare at least a redirect URI so
that astakos will be able to validate the redirect URI provided during the
authorization code request. If a client is trusted (like a pithos view) astakos
grants access on behalf of the resource owner, otherwise the resource owner has
to be asked.
We can register an oauth 2.0 client with the following command::
snf-manage oauth2-client-add <client_id> --secret=<secret> --is-trusted --url <redirect_uri>
For example::
snf-manage oauth2-client-add pithos-view --secret=12345 --is-trusted --url https://pithos.synnefo.live/pithos/ui/view
Configure view credentials in pithos
====================================
To set the credentials issued to pithos view in order to authenticate itself
with astakos during the resource access token generation procedure we have to
change the ``PITHOS_OAUTH2_CLIENT_CREDENTIALS`` setting.
The value should be a (<client_id>, <client_secret>) tuple.
For example::
PITHOS_OAUTH2_CLIENT_CREDENTIALS = ('pithos-view', 12345)
Authorization Code Grant Flow
=============================
The general flow includes the following steps:
#. The user requests to view the content of the protected resource.
#. The view requests an authorisation code from astakos by providing its
identifier, the requested scope, and a redirection URI.
#. Astakos authenticates the user and validates that the redirection URI
matches with the registered redirect URIs of the view.
As far as the pithos view is considered a trusted client, astakos grants the
access request on behalf of the user.
#. Astakos redirects the user-agent back to the view using the redirection URI
provided earlier. The redirection URI includes an authorisation code.
#. The view requests an access token from astakos by including the
authorisation code in the request. The view also posts its client identifier
and its client secret in order to authenticate itself with astakos. It also
supplies the redirection URI used to obtain the authorisation code for
verification.
#. Astakos authenticates the view, validates the authorization code,
and ensures that the redirection URI received matches the URI
used to redirect the client.
If valid, astakos responds back with an short-term access token.
#. The view exchanges with astakos the access token for the information of the
user to whom the authoritativeness was granted.
#. The view responds with the resource contents if the user has access to the
specific resource.
Authorization code request
==========================
The view receives a request without either an access token or an authorization
code. In that case it redirects to astakos's authorization endpoint by adding
the following parameters to the query component using the
"application/x-www-form-urlencoded" format:
response_type:
'code'
client_id:
'pithos-view'
redirect_uri:
the absolute path of the view request
scope:
the user specific part of the view request path
For example, the client directs the user-agent to make the following HTTP
request using TLS (with extra line breaks for display purposes only)::
GET /astakos/oauth2/auth?response_type=code&client_id=pithos-view
&redirect_uri=https%3A//pithos.synnefo.live/pithos/ui/view/b0ee4760-9451-4b9a-85f0-605c48bebbdd/pithos/image.png
&scope=/b0ee4760-9451-4b9a-85f0-605c48bebbdd/pithos/image.png HTTP/1.1
Host: accounts.synnefo.live
Access token request
====================
Astakos's authorization endpoint responses to a valid authorization code
request by redirecting the user-agent back to the requested view
(redirect_uri parameter).
The view receives the request which includes the authorization code and
makes a POST request to the astakos's token endpoint by sending the following
parameters using the "application/x-www-form-urlencoded" format in the HTTP
request entity-body:
grant_type:
"authorization_code"
code:
the authorization code received from the astakos.
redirect_uri:
the "redirect_uri" parameter was included in the authorization request
Since the pithos view is registered as a confidential client it MUST
authenticate with astakos by providing an Authorization header including the
encoded client credentials as described in
http://tools.ietf.org/html/rfc2617#page-11.
For example, the view makes the following HTTP request using TLS (with extra
line breaks for display purposes only)::
POST /astakos/oauth2/token HTTP/1.1
Host: accounts.synnefo.live
Authorization: Basic cGl0aG9zLXZpZXc6MTIzNDU=
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A//pithos.synnefo.live/pithos/ui/view/b0ee4760-9451-4b9a-85f0-605c48bebbdd/pithos/image.png
Access to the protected resource
================================
Astakos's token endpoint replies to a valid token request with a (200 OK)
response::
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"Bearer",
"expires_in":20
}
The view redirects the user-agent to itself by adding to the query component
the access token.
The view receives the request which includes an access token and requests
from astakos to validate the token by making a GET HTTP request to the
astakos's validation endpoint::
GET /astakos/identity/v2.0/tokens/2YotnFZFEjr1zCsicMWpAA?belongsTo=/b0ee4760-9451-4b9a-85f0-605c48bebbdd/pithos/image.png HTTP/1.1
Host: accounts.synnefo.live
The astakos's validation endpoint checks whether the token is valid, has not
expired and that the ``belongsTo`` parameter matches with the ``scope``
parameter that was included in the token request.
If not valid returns a 404 NOT FOUND response.
If valid, returns the information of the user to whom the token was assigned.
In the former case the view redirects to the requested path
(without the access token or the authorization code) in order to re-initiate
the procedure by requesting an new authorization code.
In the latter case the view proceeds with the request and if the user has access
to the requested resource the resource's data are returned, otherwise the
access to resource is forbidden.
Authorization code and access token invalidation
================================================
Authorization codes can be used only once (they are deleted after a
successful token creation)
Token expiration can be set by changing the ``OAUTH2_TOKEN_EXPIRES`` setting.
By default it is set to 20 seconds.
Tokens granted to a user are deleted after user logout or authentication token
renewal.
Expired tokens presented to the validation endpoint are also deleted.
Authorization code and access token length
==========================================
Authorization code length is adjustable by the
``OAUTH2_AUTHORIZATION_CODE_LENGTH`` setting. By default it is set to
60 characters.
Token length is adjustable by the ``OAUTH2_TOKEN_LENGTH`` setting.
By default it is set to 30 characters.
Restrict file serving endpoints to a specific host
==================================================
A new setting ``PITHOS_SERVE_API_DOMAIN`` has been introduced. When set,
all api views that serve pithos file contents will be restricted to be served
only under the domain specified in the setting value.
If an invalid host is identified and request HTTP method is one
of ``GET``, ``HOST``, the server will redirect using a clone of the request
with host replaced to the one the restriction applies to.
......@@ -137,6 +137,7 @@ Drafts
Resource-pool projects design <design/resource-pool-projects>
Resource defaults design <design/resource-defaults>
Pithos view authorization <design/pithos-view-authorization.rst>
Contact
......
......@@ -918,6 +918,23 @@ numeric value, i.e. 10240 MB, 10 GB etc.
# snf-manage resource-modify --default-quota-interactive
.. _pithos_view_registration:
Register pithos view as an OAuth 2.0 client
-------------------------------------------
Starting from synnefo version 0.15, the pithos view, in order to get access to
the data of a protect pithos resource, has to be granted authorization for the
specific resource by astakos.
During the authorization grant procedure, it has to authenticate itself with
astakos since the later has to prevent serving requests by unknown/unauthorized
clients.
To register the pithos view as an OAuth 2.0 client in astakos, we have to run
the following command::
snf-manage oauth2-client-add pithos-view --secret=<secret> --is-trusted --url https://node2.example.com/pithos/ui/view
Servers Initialization
----------------------
......@@ -1075,6 +1092,13 @@ The ``CLOUDBAR_SERVICES_URL`` and ``CLOUDBAR_MENU_URL`` options are used by the
Pithos web client to get from astakos all the information needed to fill its
own cloudbar. So we put our astakos deployment urls there.
The ``PITHOS_OAUTH2_CLIENT_CREDENTIALS`` setting is used by the pithos view
in order to authenticate itself with astakos during the authorization grant
procedure and it should container the credentials issued for the pithos view
in `the pithos view registration step`__.
__ pithos_view_registration_
Pooling and Greenlets
---------------------
......
......@@ -143,7 +143,25 @@ The upgrade to v0.15 consists in the following steps:
pithos-host$ pithos-migrate upgrade head
2.3 Update configuration files
.. _pithos_view_registration:
2.3 Register pithos view as an oauth 2.0 client in astakos
----------------------------------------------------------
Starting from synnefo version 0.15, the pithos view, in order to get access to
the data of a protect pithos resource, has to be granted authorization for the
specific resource by astakos.
During the authorization grant procedure, it has to authenticate itself with
astakos since the later has to prevent serving requests by unknown/unauthorized
clients.
To register the pithos view as an OAuth 2.0 client in astakos, use the
following command::
snf-manage oauth2-client-add pithos-view --secret=<secret> --is-trusted --url https://pithos.synnefo.live/pithos/ui/view
2.4 Update configuration files
------------------------------
The ``ASTAKOS_BASE_URL`` setting has been replaced (both in Cyclades and
......@@ -240,6 +258,12 @@ value / string and make sure that it's the same as the ``STATS_SECRET_KEY``
setting (used to decrypt the instance hostname) in
``20-snf-stats-settings.conf`` on your Stats host.
In addition to this, we have to change the ``PITHOS_OAUTH2_CLIENT_CREDENTIALS``
setting in the ``20-snf-pithos-app-settings.conf`` file to set the credentials
issued for the pithos view in `the previous step`__.
__ pithos_view_registration_
3. Create floating IP pools
===========================
......
......@@ -36,6 +36,8 @@ from snf_django.lib.api import api_endpoint_not_found
urlpatterns = patterns(
'astakos.api.tokens',
url(r'^v2.0/tokens/(?P<token_id>.+?)?$', 'validate_token',
name='validate_token'),
url(r'^v2.0/tokens/?$', 'authenticate', name='tokens_authenticate'),
url(r'^.*', api_endpoint_not_found),
)
......@@ -96,5 +96,9 @@ astakos_services = {
{'versionId': '',
'publicURL': None},
],
'resources': {},
},
}
from astakos.oa2.services import oa2_services
astakos_services.update(oa2_services)
......@@ -40,6 +40,8 @@ from django.core.cache import cache
from astakos.im import settings
from astakos.im.models import Service, AstakosUser
from astakos.oa2.backends.base import OA2Error
from astakos.oa2.backends.djangobackend import DjangoBackend
from .util import json_response, xml_response, validate_user,\
get_content_length
......@@ -137,3 +139,35 @@ def authenticate(request):
return xml_response({'d': d}, 'api/access.xml')
else:
return json_response(d)
@api_method(http_method="GET", token_required=False, user_required=False,
logger=logger)
def validate_token(request, token_id):
oa2_backend = DjangoBackend()
try:
token = oa2_backend.consume_token(token_id)
except OA2Error, e:
raise faults.ItemNotFound(e.message)
belongsTo = request.GET.get('belongsTo')
if belongsTo is not None:
if not belongsTo.startswith(token.scope):
raise faults.ItemNotFound(
"The specified tenant is outside the token's scope")
d = defaultdict(dict)
d["access"]["token"] = {"id": token.code,
"expires": token.expires_at,
"tenant": {"id": token.user.uuid,
"name": token.user.realname}}
d["access"]["user"] = {"id": token.user.uuid,
'name': token.user.realname,
"roles": list(token.user.groups.values("id",
"name")),
"roles_links": []}
if request.serialization == 'xml':
return xml_response({'d': d}, 'api/access.xml')
else:
return json_response(d)
......@@ -73,6 +73,7 @@ def login(request, user):
def logout(request, *args, **kwargs):
user = request.user
auth_logout(request, *args, **kwargs)
user.delete_online_access_tokens()
logger.info('%s logged out.', user.log_display)
......
......@@ -562,6 +562,7 @@ class AstakosUser(User):
timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
if flush_sessions:
self.flush_sessions(current_key)
self.delete_online_access_tokens()
msg = 'Token renewed for %s'
logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
......@@ -812,6 +813,12 @@ class AstakosUser(User):
return False
return True
def delete_online_access_tokens(self):
offline_tokens = self.token_set.filter(access_token='online')
logger.info('The following access tokens will be deleted: %s',
offline_tokens)
offline_tokens.delete()
class AstakosUserAuthProviderManager(models.Manager):
......
......@@ -33,6 +33,8 @@
from astakos.im.tests.common import *
from astakos.im.settings import astakos_services, BASE_HOST
from astakos.oa2.backends import DjangoBackend
from synnefo.lib.services import get_service_path
from synnefo.lib import join_urls
......@@ -43,6 +45,7 @@ from datetime import date
#from xml.dom import minidom
import json
import time
ROOT = "/%s/%s/%s/" % (
astakos_settings.BASE_PATH, astakos_settings.ACCOUNTS_PREFIX, 'v1.0')
......@@ -446,6 +449,14 @@ class TokensApiTest(TestCase):
e3.data.create(key='versionId', value='v2.0')
e3.data.create(key='publicURL', value='http://localhost:8000/s3/v2.0')
oa2_backend = DjangoBackend()
self.token = oa2_backend.token_model.create(
code='12345',
expires_at=datetime.now() + timedelta(seconds=5),
user=self.user1,
client=oa2_backend.client_model.create(type='public'),
redirect_uri='https://server.com/handle_code')
def test_authenticate(self):
client = Client()
url = reverse('astakos.api.tokens.authenticate')
......@@ -570,7 +581,7 @@ class TokensApiTest(TestCase):
r = client.post(url, post_data, content_type='application/json')
self.assertEqual(r.status_code, 200)
# Check successful json response
# Check successful json response: user credential auth
post_data = """{"auth":{"passwordCredentials":{"username":"%s",
"password":"%s"},
"tenantName":"%s"}}""" % (
......@@ -594,6 +605,29 @@ class TokensApiTest(TestCase):
self.assertEqual(user, self.user1.uuid)
self.assertEqual(len(service_catalog), 3)
# Check successful json response: token auth
post_data = """{"auth":{"token":{"id":"%s"},
"tenantName":"%s"}}""" % (
self.user1.auth_token, self.user1.uuid)
r = client.post(url, post_data, content_type='application/json')
self.assertEqual(r.status_code, 200)
self.assertTrue(r['Content-Type'].startswith('application/json'))
try:
body = json.loads(r.content)
except Exception, e:
self.fail(e)