Commit d8a59a0f authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Merge branch 'feature-use-astakosclient' into feature-quotas

parents df0a350d 45429bbd
......@@ -24,11 +24,14 @@ Astakos
Cyclades
--------
* Remove 'CYCLADES_USER_CATALOG_URL' and 'CYCLADES_USER_FEEDBACK_URL' settings
Pithos
------
* Remove PITHOS_AUTHENTICATION_USERS setting, which was used to override
astakos users.
* Remove 'PITHOS_USER_CATALOG_URL', 'PITHOS_USER_FEEDBACK_URL' and
'PITHOS_USER_LOGIN_URL' settings.
Tools
-----
......
......@@ -854,9 +854,6 @@ this options:
PITHOS_SERVICE_TOKEN = 'pithos_service_token22w=='
PITHOS_USER_CATALOG_URL = 'https://node1.example.com/user_catalogs'
PITHOS_USER_FEEDBACK_URL = 'https://node1.example.com/feedback'
PITHOS_USER_LOGIN_URL = 'https://node1.example.com/login'
PITHOS_QUOTAHOLDER_URL = 'https://node1.example.com/quotaholder/v'
PITHOS_QUOTAHOLDER_TOKEN = 'aExampleTokenJbFm12w'
......
......@@ -113,8 +113,6 @@ In `/etc/synnefo/cyclades.conf` add:
CYCLADES_ASTAKOS_SERVICE_TOKEN = "XXXXXXXXXX"
CYCLADES_USER_CATALOG_URL = 'https://accounts.example.com/user_catalogs'
UI_SYSTEM_IMAGES_OWNERS = {
'admin@synnefo.gr': 'system',
'images@synnefo.gr': 'system'
......
......@@ -58,9 +58,6 @@ In `/etc/synnefo/pithos.conf` add:
.. code-block:: console
ASTAKOS_URL = 'https:/accounts.example.com/'
PITHOS_USER_CATALOG_URL = 'https://accounts.example.com/user_catalogs'
PITHOS_USER_FEEDBACK_URL = 'https://accounts.example.com/feedback'
PITHOS_USER_LOGIN_URL = 'https://accounts.example.com/login'
PITHOS_BACKEND_DB_CONNECTION = 'postgresql://synnefo:example_passw0rd@db.example.com:5432/snf_pithos'
PITHOS_BACKEND_BLOCK_PATH = '/srv/pithos/data'
......
......@@ -123,14 +123,8 @@
## The token used for astakos service api calls (e.g. api to retrieve user email
## using a user uuid)
#CYCLADES_ASTAKOS_SERVICE_TOKEN = ''
#
## Astakos user_catalogs endpoint
#CYCLADES_USER_CATALOG_URL = 'https://<astakos domain>/user_catalogs'
# Let cyclades proxy user specific api calls to astakos, via self served
# endpoints. Set this to False if you deploy cyclades-app/astakos-app on the
# same machine.
#CYCLADES_PROXY_USER_SERVICES = True
#
## Astakos feedback endpoint.
#CYCLADES_USER_FEEDBACK_URL = 'https://accounts.example.synnefo.org/feedback'
......@@ -76,6 +76,7 @@ INSTALL_REQUIRES = [
'setproctitle>=1.0.1',
'bitarray>=0.8',
'objpool>=0.2',
'astakosclient',
'snf-django-lib'
]
......
......@@ -39,8 +39,9 @@ from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
USER_CATALOG_URL = getattr(settings, 'CYCLADES_USER_CATALOG_URL', None)
USER_FEEDBACK_URL = getattr(settings, 'CYCLADES_USER_FEEDBACK_URL', None)
ASTAKOS_URL = getattr(settings, 'ASTAKOS_URL', None)
USER_CATALOG_URL = urlparse.urljoin(ASTAKOS_URL, "user_catalogs")
USER_FEEDBACK_URL = urlparse.urljoin(ASTAKOS_URL, "feedback")
from objpool.http import PooledHTTPConnection
......
......@@ -124,13 +124,7 @@ SECRET_ENCRYPTION_KEY= "Password Encryption Key"
# using a user uuid)
CYCLADES_ASTAKOS_SERVICE_TOKEN = ''
# Astakos user_catalogs endpoint
CYCLADES_USER_CATALOG_URL = 'https://<astakos domain>/user_catalogs'
# Let cyclades proxy user specific api calls to astakos, via self served
# endpoints. Set this to False if you deploy cyclades-app/astakos-app on the
# same machine.
CYCLADES_PROXY_USER_SERVICES = True
# Astakos user_catalogs endpoint
CYCLADES_USER_FEEDBACK_URL = 'https://accounts.example.synnefo.org/feedback'
......@@ -18,7 +18,3 @@ DEFAULT_CONTAINER_FORMAT = 'bare'
# The owner of the images that will be marked as "system images" by the UI
SYSTEM_IMAGES_OWNER = 'okeanos'
# If true, this enables a ui compatibility layer for the introduction of UUIDs
# in identity management. WARNING: Setting to True will break your installation.
TRANSLATE_UUIDS = False
......@@ -202,6 +202,3 @@ UI_SYSTEM_IMAGES_OWNERS = {
'admin@synnefo.gr': 'system',
'images@synnefo.gr': 'system'
}
# Astakos feedback endpoint. UI uses this setting to post error feedbacks
CYCLADES_USER_FEEDBACK_URL = 'https://accounts.synnefo.org/feedback'
......@@ -51,6 +51,24 @@ USERS_DISPLAYNAMES = dict(map(lambda k: (k[1]['displayname'], {'uuid': k[0]}),
from synnefo.db import models_factory as mfactory
class AstakosClientMock():
def __init__(*args, **kwargs):
pass
def get_username(self, token, uuid):
try:
return USERS_UUIDS.get(uuid)['displayname']
except TypeError:
return None
def get_uuid(self, token, display_name):
try:
return USERS_DISPLAYNAMES.get(display_name)['uuid']
except TypeError:
return None
class AuthClient(Client):
def request(self, **request):
......@@ -79,23 +97,11 @@ class HelpdeskTests(TestCase):
'helpdesk'],
'auth_token': '0001'}
def get_uuid_mock(token, displayname, url):
try:
return USERS_DISPLAYNAMES.get(displayname)['uuid']
except TypeError:
return None
def get_displayname_mock(token, uuid, url):
try:
return USERS_UUIDS.get(uuid)['displayname']
except TypeError:
return None
# mock the astakos authentication function
from snf_django.lib import astakos
astakos.get_user = get_user_mock
astakos.get_displayname = get_displayname_mock
astakos.get_user_uuid = get_uuid_mock
import astakosclient
astakosclient.AstakosClient = AstakosClientMock
settings.SKIP_SSH_VALIDATION = True
settings.HELPDESK_ENABLED = True
......
......@@ -45,7 +45,7 @@ from urllib import unquote
from snf_django.lib.astakos import get_user
from synnefo.db.models import VirtualMachine, NetworkInterface, Network
from snf_django.lib import astakos
from astakosclient import AstakosClient
# server actions specific imports
from synnefo.api import servers
......@@ -58,7 +58,6 @@ UUID_SEARCH_REGEX = re.compile('([0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12})')
VM_SEARCH_REGEX = re.compile('vm(-){0,}(?P<vmid>[0-9]+)')
def get_token_from_cookie(request, cookiename):
"""
Extract token from the cookie name provided. Cookie should be in the same
......@@ -81,11 +80,6 @@ AUTH_COOKIE_NAME = getattr(settings, 'HELPDESK_AUTH_COOKIE_NAME',
PERMITTED_GROUPS = getattr(settings, 'HELPDESK_PERMITTED_GROUPS', ['helpdesk'])
SHOW_DELETED_VMS = getattr(settings, 'HELPDESK_SHOW_DELETED_VMS', False)
# guess cyclades setting too
USER_CATALOG_URL = getattr(settings, 'CYCLADES_USER_CATALOG_URL', None)
USER_CATALOG_URL = getattr(settings, 'HELPDESK_USER_CATALOG_URL',
USER_CATALOG_URL)
def token_check(func):
"""
......@@ -115,7 +109,8 @@ def helpdesk_user_required(func, permitted_groups=PERMITTED_GROUPS):
raise Http404
token = get_token_from_cookie(request, AUTH_COOKIE_NAME)
get_user(request, settings.ASTAKOS_URL, fallback_token=token)
get_user(request, settings.ASTAKOS_URL,
fallback_token=token, logger=logger)
if hasattr(request, 'user') and request.user:
groups = request.user.get('groups', [])
......@@ -199,15 +194,16 @@ def account(request, search_query):
account = None
search_query = vmid
astakos = AstakosClient(settings.ASTAKOS_URL, retry=2,
use_pool=True, logger=logger)
if is_uuid:
account = search_query
account_name = astakos.get_displayname(auth_token, account,
USER_CATALOG_URL)
account_name = astakos.get_username(auth_token, account)
if account_exists and not is_uuid:
account_name = search_query
account = astakos.get_user_uuid(auth_token, account_name,
USER_CATALOG_URL)
account = astakos.get_uuid(auth_token, account_name)
if not account:
account_exists = False
......
......@@ -33,9 +33,7 @@
# or implied, of GRNET S.A.
from django import http
from django.template import RequestContext, loader
from django.utils import simplejson as json
from django.core import serializers
from django.core.urlresolvers import reverse
from django.http import HttpResponse
......@@ -44,9 +42,6 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from snf_django.lib.astakos import get_user
from django.conf import settings
# base view class
# https://github.com/bfirsh/django-class-based-views/blob/master/class_based_views/base.py
class View(object):
"""
......@@ -82,7 +77,7 @@ class View(object):
Main entry point for a request-response process.
"""
def view(request, *args, **kwargs):
user = get_user(request, settings.ASTAKOS_URL)
get_user(request, settings.ASTAKOS_URL)
if not request.user_uniq:
return HttpResponse(status=401)
self = cls(*initargs, **initkwargs)
......@@ -100,20 +95,24 @@ class View(object):
if request.method.upper() in ['POST', 'PUT']:
# Expect json data
if request.META.get('CONTENT_TYPE').startswith('application/json'):
if request.META.get('CONTENT_TYPE').startswith(
'application/json'):
try:
data = json.loads(data)
except ValueError:
return http.HttpResponseServerError('Invalid JSON data.')
return \
http.HttpResponseServerError('Invalid JSON data.')
else:
return http.HttpResponseServerError('Unsupported Content-Type.')
return http.HttpResponseServerError(
'Unsupported Content-Type.')
try:
return getattr(self, request.method.upper())(request, data, *args, **kwargs)
return getattr(self, request.method.upper())(
request, data, *args, **kwargs)
except ValidationError, e:
# specific response for validation errors
return http.HttpResponseServerError(json.dumps({'errors':
e.message_dict, 'non_field_key':
NON_FIELD_ERRORS }))
return http.HttpResponseServerError(
json.dumps({'errors': e.message_dict,
'non_field_key': NON_FIELD_ERRORS}))
else:
allowed_methods = [m for m in self.method_names if hasattr(self, m)]
......@@ -126,6 +125,7 @@ class JSONRestView(View):
"""
url_name = None
def __init__(self, url_name, *args, **kwargs):
self.url_name = url_name
return super(JSONRestView, self).__init__(*args, **kwargs)
......@@ -184,8 +184,8 @@ class ResourceView(JSONRestView):
raise http.Http404
def GET(self, request, data, *args, **kwargs):
return self.json_response(self.instance_to_dict(self.instance(),
self.exclude_fields))
return self.json_response(
self.instance_to_dict(self.instance(), self.exclude_fields))
def PUT(self, request, data, *args, **kwargs):
instance = self.instance()
......@@ -209,16 +209,16 @@ class CollectionView(JSONRestView):
return self.model.objects.all()
def GET(self, request, data, *args, **kwargs):
return self.json_response(list(self.qs_to_dict_iter(self.queryset(),
self.exclude_fields)))
return self.json_response(
list(self.qs_to_dict_iter(self.queryset(), self.exclude_fields)))
def POST(self, request, data, *args, **kwargs):
instance = self.model()
self.update_instance(instance, data, self.exclude_fields)
instance.full_clean()
instance.save()
return self.json_response(self.instance_to_dict(instance,
self.exclude_fields))
return self.json_response(
self.instance_to_dict(instance, self.exclude_fields))
class UserResourceView(ResourceView):
......@@ -227,7 +227,7 @@ class UserResourceView(ResourceView):
"""
def queryset(self):
return super(UserResourceView,
self).queryset().filter(user=self.request.user_uniq)
self).queryset().filter(user=self.request.user_uniq)
class UserCollectionView(CollectionView):
......@@ -235,7 +235,8 @@ class UserCollectionView(CollectionView):
Filter collection queryset for request user entries
"""
def queryset(self):
return super(UserCollectionView, self).queryset().filter(user=self.request.user_uniq)
return super(UserCollectionView,
self).queryset().filter(user=self.request.user_uniq)
def POST(self, request, data, *args, **kwargs):
instance = self.model()
......@@ -243,5 +244,5 @@ class UserCollectionView(CollectionView):
instance.user = request.user_uniq
instance.full_clean()
instance.save()
return self.json_response(self.instance_to_dict(instance,
self.exclude_fields))
return self.json_response(
self.instance_to_dict(instance, self.exclude_fields))
......@@ -33,7 +33,6 @@
# or implied, of GRNET S.A.
from django import http
from django.template import RequestContext, loader
from django.utils import simplejson as json
from django.conf import settings
......@@ -51,15 +50,20 @@ except ImportError, e:
import base64
class PublicKeyPairResourceView(rest.UserResourceView):
model = PublicKeyPair
exclude_fields = ["user"]
class PublicKeyPairCollectionView(rest.UserCollectionView):
model = PublicKeyPair
exclude_fields = ["user"]
SSH_KEY_LENGTH = getattr(settings, 'USERDATA_SSH_KEY_LENGTH', 2048)
def generate_key_pair(request):
"""
Response to generate private/public RSA key pair
......@@ -74,14 +78,13 @@ def generate_key_pair(request):
raise Exception("Application does not support ssh keys generation")
if PublicKeyPair.user_limit_exceeded(request.user):
raise http.HttpResponseServerError("SSH keys limit exceeded");
raise http.HttpResponseServerError("SSH keys limit exceeded")
# generate RSA key
from Crypto import Random
Random.atfork()
key = rsakey.RSA.generate(SSH_KEY_LENGTH);
key = rsakey.RSA.generate(SSH_KEY_LENGTH)
# get PEM string
pem = exportKey(key, 'PEM')
......@@ -97,6 +100,7 @@ def generate_key_pair(request):
data = {'private': pem, 'public': public}
return http.HttpResponse(json.dumps(data), mimetype="application/json")
def download_private_key(request):
"""
Return key contents
......@@ -108,4 +112,3 @@ def download_private_key(request):
response['Content-Disposition'] = 'attachment; filename=%s' % name
response.write(data)
return response
This diff is collapsed.
......@@ -90,15 +90,10 @@ def api_method(http_method=None, token_required=True, user_required=True,
if user_required:
assert(token_required), "Can not get user without token"
astakos = astakos_url or settings.ASTAKOS_URL
try:
astakos = AstakosClient(astakos,
use_pool=True,
logger=logger)
user_info = astakos.get_user_info(token)
except AstakosClientException as err:
raise faults.Fault(message=err.message,
details=err.details,
code=err.status)
astakos = AstakosClient(astakos,
use_pool=True,
logger=logger)
user_info = astakos.get_user_info(token)
request.user_uniq = user_info["uuid"]
request.user = user_info
......@@ -112,6 +107,13 @@ def api_method(http_method=None, token_required=True, user_required=True,
if fault.code >= 500:
logger.exception("API ERROR")
return render_fault(request, fault)
except AstakosClientException as err:
fault = faults.Fault(message=err.message,
details=err.details,
code=err.status)
if fault.code >= 500:
logger.exception("Astakos ERROR")
return render_fault(request, fault)
except:
logger.exception("Unexpected ERROR")
fault = faults.InternalServerError("Unexpected ERROR")
......
......@@ -31,180 +31,35 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import logging
from astakosclient import AstakosClient
from astakosclient.errors import Unauthorized
from urlparse import urlparse
from urllib import unquote
from django.utils import simplejson as json
from objpool.http import PooledHTTPConnection
logger = logging.getLogger(__name__)
def retry(howmany):
def execute(func):
def f(*args, **kwargs):
attempts = 0
while True:
try:
return func(*args, **kwargs)
except Exception, e:
is_last_attempt = attempts == howmany - 1
if is_last_attempt:
raise e
if e.args:
status = e.args[-1]
# In case of Unauthorized response
# or Not Found return directly
if status == 401 or status == 404:
raise e
attempts += 1
return f
return execute
def call(token, url, headers=None, body=None, method='GET'):
p = urlparse(url)
kwargs = {}
if headers is None:
headers = {}
kwargs["headers"] = headers
kwargs['headers']['X-Auth-Token'] = token
if body:
kwargs['body'] = body
kwargs['headers'].setdefault('content-type',
'application/octet-stream')
kwargs['headers'].setdefault('content-length', len(body) if body else 0)
with PooledHTTPConnection(p.netloc, p.scheme) as conn:
conn.request(method, p.path + '?' + p.query, **kwargs)
response = conn.getresponse()
headers = response.getheaders()
headers = dict((unquote(h), unquote(v)) for h, v in headers)
length = response.getheader('content-length', None)
data = response.read(length)
status = int(response.status)
if status < 200 or status >= 300:
raise Exception(data, status)
return json.loads(data)
def authenticate(
token, authentication_url='http://127.0.0.1:8000/im/authenticate',
usage=False):
if usage:
authentication_url += "?usage=1"
return call(token, authentication_url)
@retry(3)
def get_displaynames(
token,
uuids,
url='http://127.0.0.1:8000/user_catalogs',
override_users={}):
if override_users:
return dict((u, u) for u in uuids)
try:
data = call(
token, url, headers={'content-type': 'application/json'},
body=json.dumps({'uuids': uuids}), method='POST')
except:
raise
else:
return data.get('uuid_catalog')
@retry(3)
def get_uuids(
token,
displaynames,
url='http://127.0.0.1:8000/user_catalogs',
override_users={}):
if override_users:
return dict((u, u) for u in displaynames)
try:
data = call(
token, url, headers={'content-type': 'application/json'},
body=json.dumps({'displaynames': displaynames}), method='POST')
except:
raise
else:
return data.get('displayname_catalog')
def get_user_uuid(
token,
displayname,
url='http://127.0.0.1:8000/user_catalogs',
override_users={}):
if not displayname:
return
displayname_dict = get_uuids(token, [displayname], url, override_users)
return displayname_dict.get(displayname)
def get_displayname(
token,
uuid,
url='http://127.0.0.1:8000/user_catalogs',
override_users={}):
if not uuid:
return
uuid_dict = get_displaynames(token, [uuid], url, override_users)
return uuid_dict.get(uuid)
def user_for_token(token, authentication_url, usage=False):
def user_for_token(client, token, usage=False):
if not token:
return None
try:
return authenticate(token, authentication_url, usage=usage)
except Exception, e:
# In case of Unauthorized response return None
if e.args and e.args[-1] == 401:
return None
raise e
return client.get_user_info(token, usage=True)
except Unauthorized:
return None
def get_user(
request,