Commit e10a4628 authored by Georgios D. Tsoukalas's avatar Georgios D. Tsoukalas
Browse files

Merge branch 'feature-projects' into develop

parents c206640b 7063e203
This diff is collapsed.
......@@ -58,6 +58,10 @@ In `/etc/synnefo/pithos.conf` add:
PITHOS_AUTHENTICATION_URL = 'https:/accounts.example.com/im/authenticate'
PITHOS_AUTHENTICATION_USERS = None
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'
PITHOS_BACKEND_QUOTA = 20 * 1024 * 1024 * 1024
......
......@@ -27,6 +27,9 @@ Document Revisions
========================= ================================
Revision Description
========================= ================================
0.13 (Jun 21, 2013) Proxy identity management services
\ Uuid to displayname translation
0.9 (Feb 17, 2012) Change permissions model.
0.10 (Jul 18, 2012) Support for bulk COPY/MOVE/DELETE
\ Optionally include public objects in listings.
0.9 (Feb 17, 2012) Change permissions model.
......@@ -72,7 +75,7 @@ Revision Description
\ Create object using hashmap.
0.3 (June 14, 2011) Large object support with ``X-Object-Manifest``.
\ Allow for publicly available objects via ``https://hostname/public``.
\ Support time-variant account/container listings.
\ Support time-variant account/container listings.
\ Add source version when duplicating with ``PUT``/``COPY``.
\ Request version in object ``HEAD``/``GET`` requests (list versions with ``GET``).
0.2 (May 31, 2011) Add object meta listing and filtering in containers.
......@@ -106,6 +109,79 @@ When done with logging in, the service's login URI should redirect to the URI pr
A user management service that implements a login URI according to these conventions is Astakos (https://code.grnet.gr/projects/astakos), by GRNET.
User feedback
-------------
Client software using Pithos, should forward to the ``/feedback`` URI. The Pithos service, depending on its configuration will delegate the request to the appropriate identity management URI.
====================== =========================
Request Parameter Name Value
====================== =========================
feedback_msg Feedback message
feedback_data Additional information about service client status
====================== =========================
|
==================== ===========================
Request Header Name Value
==================== ===========================
X-Auth-Token User authentication token
==================== ===========================
|
=========================== =====================
Return Code Description
=========================== =====================
200 (OK) The request succeeded
502 (Bad Gateway) Send feedback failure
400 (Bad Request) Method not allowed or invalid message data
401 (Unauthorized) Missing or expired user token
500 (Internal Server Error) The request cannot be completed because of an internal error
=========================== =====================
User translation catalogs
-------------------------
Client software using Pithos, should forward to the ``/user_catalogs`` URI to get uuid to displayname translations and vice versa. The Pithos service, depending on its configuration will delegate the request to the appropriate identity management URI.
==================== ===========================
Request Header Name Value
==================== ===========================
X-Auth-Token User authentication token
==================== ===========================
The request body is a json formatted dictionary containing a list with uuids and another list of displaynames to translate.
Example request content:
::
{"displaynames": ["user1@example.com", "user2@example.com"],
"uuids":["ff53baa9-c025-4d56-a6e3-963db0438830", "a9dc21d2-bcb2-4104-9a9e-402b7c70d6d8"]}
Example reply:
::
{"displayname_catalog": {"user1@example.com": "a9dc21d2-bcb2-4104-9a9e-402b7c70d6d8",
"user2@example.com": "816351c7-7405-4f26-a968-6380cf47ba1f"},
'uuid_catalog': {"a9dc21d2-bcb2-4104-9a9e-402b7c70d6d8": "user1@example.com",
"ff53baa9-c025-4d56-a6e3-963db0438830": "user2@example.com"}}
|
=========================== =====================
Return Code Description
=========================== =====================
200 (OK) The request succeeded
400 (Bad Request) Method not allowed or request body is not json formatted
401 (Unauthorized) Missing or expired or invalid user token
500 (Internal Server Error) The request cannot be completed because of an internal error
=========================== =====================
The Pithos API
--------------
......
......@@ -271,7 +271,6 @@ containing the following:
RewriteEngine On
RewriteCond %{THE_REQUEST} ^.*(\\r|\\n|%0A|%0D).* [NC]
RewriteRule ^(.*)$ - [F,L]
RewriteRule ^/login(.*) /im/login/redirect$1 [PT,NE]
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
......@@ -822,6 +821,10 @@ this options:
PITHOS_AUTHENTICATION_USERS = None
PITHOS_SERVICE_TOKEN = 'pithos_service_token22w=='
PITHOS_USER_CATALOG_URL = 'http://node1.example.com/user_catalogs'
PITHOS_USER_FEEDBACK_URL = 'http://node1.example.com/feedback'
PITHOS_USER_LOGIN_URL = 'http://node1.example.com/login'
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
......@@ -854,7 +857,7 @@ Then we need to setup the web UI and connect it to astakos. To do so, edit
.. code-block:: console
PITHOS_UI_LOGIN_URL = "https://node1.example.com/im/login?next="
PITHOS_UI_FEEDBACK_URL = "https://node1.example.com/im/feedback"
PITHOS_UI_FEEDBACK_URL = "https://node2.example.com/feedback"
The ``PITHOS_UI_LOGIN_URL`` option tells the client where to redirect you, if
you are not logged in. The ``PITHOS_UI_FEEDBACK_URL`` option points at the
......
import os
import sys
os.environ['DJANGO_SETTINGS_MODULE'] = 'synnefo.settings'
from django.conf import settings
from astakos.im.models import AstakosUser
def duplicate_users():
for u in AstakosUser.objects.filter():
if AstakosUser.objects.filter(email__iexact=u.email).count() > 1:
print AstakosUser.objects.filter(email__iexact=u.email).values('pk',
'email',
'is_active')
if len(sys.argv) == 2:
pk = int(sys.argv[1])
user = AstakosUser.objects.get(pk=pk)
if AstakosUser.objects.filter(email__iexact=user.email).count() == 1:
print "No duplicate emails found for user %s" % (user)
exit()
user = AstakosUser.objects.get(pk=pk)
print "Deleting user %r" % (user)
user.delete()
exit()
else:
duplicate_users()
......@@ -39,11 +39,15 @@ from django.http import HttpResponse
from django.utils import simplejson as json
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from astakos.im.models import AstakosUser, Service, Resource
from astakos.im.api.faults import Fault, ItemNotFound, InternalServerError, BadRequest
from astakos.im.api.faults import (
Fault, ItemNotFound, InternalServerError, BadRequest)
from astakos.im.settings import (
INVITATIONS_ENABLED, COOKIE_NAME, EMAILCHANGE_ENABLED, QUOTAHOLDER_URL)
from astakos.im.forms import FeedbackForm
from astakos.im.functions import send_feedback as send_feedback_func
import logging
logger = logging.getLogger(__name__)
......@@ -108,8 +112,12 @@ def get_services(request):
@api_method()
def get_menu(request, with_extra_links=False, with_signout=True):
user = request.user
from_location = request.GET.get('location')
index_url = reverse('index')
l = [{'url': absolute(request, index_url), 'name': "Sign in"}]
if from_location:
index_url = "%s?next=%s" % (index_url, from_location)
l = [{'url': absolute(request, index_url), 'name': _("Sign in")}]
if user.is_authenticated():
l = []
append = l.append
......@@ -118,25 +126,36 @@ def get_menu(request, with_extra_links=False, with_signout=True):
append(item(
url=absolute(request, reverse('index')),
name=user.email))
append(item(url=absolute(request, reverse('edit_profile')),
name="My account"))
if with_extra_links:
if EMAILCHANGE_ENABLED:
append(item(
url=absolute(request, reverse('email_change')),
name="Change email"))
append(item(
url=absolute(request, reverse('landing')),
name="Overview"))
if with_signout:
append(item(
url=absolute(request, reverse('edit_profile')),
name="Dashboard"))
if with_extra_links:
append(item(url=absolute(request, reverse('edit_profile')),
name="Profile"))
if with_extra_links:
if INVITATIONS_ENABLED:
append(item(
url=absolute(request, reverse('invite')),
name="Invitations"))
append(item(
url=absolute(request, reverse('resource_usage')),
name="Usage"))
if QUOTAHOLDER_URL:
append(item(
url=absolute(request, reverse('project_list')),
name="Projects"))
append(item(
url=absolute(request, reverse('resource_usage')),
name="Usage"))
#append(item(
#url=absolute(request, reverse('api_access')),
#name="API Access"))
append(item(
url=absolute(request, reverse('feedback')),
name="Contact"))
......@@ -189,3 +208,51 @@ class MenuItem(dict):
super(MenuItem, self).__setattribute__(name, value)
if name == 'current_path':
self.__set_is_active__()
def __get_uuid_displayname_catalogs(request, user_call=True):
# Normal Response Codes: 200
# Error Response Codes: badRequest (400)
try:
input_data = json.loads(request.raw_post_data)
except:
raise BadRequest('Request body should be json formatted.')
else:
uuids = input_data.get('uuids', [])
if uuids == None and user_call:
uuids = []
displaynames = input_data.get('displaynames', [])
if displaynames == None and user_call:
displaynames = []
d = {'uuid_catalog':AstakosUser.objects.uuid_catalog(uuids),
'displayname_catalog':AstakosUser.objects.displayname_catalog(displaynames)}
response = HttpResponse()
response.status = 200
response.content = json.dumps(d)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
def __send_feedback(request, email_template_name='im/feedback_mail.txt', user=None):
if not user:
auth_token = request.POST.get('auth', '')
if not auth_token:
raise BadRequest('Missing user authentication')
try:
user = AstakosUser.objects.get(auth_token=auth_token)
except AstakosUser.DoesNotExist:
raise BadRequest('Invalid user authentication')
form = FeedbackForm(request.POST)
if not form.is_valid():
raise BadRequest('Invalid data')
msg = form.cleaned_data['feedback_msg']
data = form.cleaned_data['feedback_data']
try:
send_feedback_func(msg, data, user, email_template_name)
except:
return HttpResponse(status=502)
return HttpResponse(status=200)
......@@ -231,7 +231,7 @@ class DjangoBackend(BaseBackend):
@safe
def get_resource_usage(self, user_id):
user = self._lookup_user(user_id)
data = get_quota(user)
data = get_quota([user])
if not data:
return ()
resources = []
......@@ -253,6 +253,8 @@ class DjangoBackend(BaseBackend):
report_desc=resource.report_desc,
placeholder=resource.placeholder,
verbose_name=resource.verbose_name,
display_name=resource.display_name,
pluralized_display_name=resource.pluralized_display_name,
maxValue=quantity + capacity,
currValue=currValue)
append(d)
......
......@@ -40,12 +40,10 @@ from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils import simplejson as json
from . import render_fault
from . import render_fault, __get_uuid_displayname_catalogs, __send_feedback
from .faults import (
Fault, Unauthorized, InternalServerError, BadRequest, ItemNotFound)
from astakos.im.models import AstakosUser, Service
from astakos.im.forms import FeedbackForm
from astakos.im.functions import send_feedback as send_feedback_func
from astakos.im.models import Service
logger = logging.getLogger(__name__)
......@@ -82,43 +80,15 @@ def api_method(http_method=None, token_required=False):
return wrapper
return decorator
@api_method(http_method='GET', token_required=True)
def get_user_info(request):
@csrf_exempt
@api_method(http_method='POST', token_required=True)
def get_uuid_displayname_catalogs(request):
# Normal Response Codes: 200
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
# itemNotFound (404)
username = request.META.get('HTTP_X_USER_USERNAME')
uuid = request.META.get('HTTP_X_USER_UUID')
if not username and not uuid:
raise BadRequest('Either username or uuid is required.')
query = AstakosUser.objects.all()
user_info = None
if username:
try:
user = query.get(username__iexact=username)
except AstakosUser.DoesNotExist:
raise ItemNotFound('Invalid username: %s' % username)
else:
user_info = {'uuid': user.uuid}
else:
try:
user = query.get(uuid=uuid)
except AstakosUser.DoesNotExist:
raise ItemNotFound('Invalid uuid: %s' % uuid)
else:
user_info = {'username': user.username}
response = HttpResponse()
response.status = 200
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
return __get_uuid_displayname_catalogs(request, user_call=False)
@csrf_exempt
@api_method(http_method='POST', token_required=True)
......@@ -127,26 +97,5 @@ def send_feedback(request, email_template_name='im/feedback_mail.txt'):
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
auth_token = request.POST.get('auth', '')
if not auth_token:
raise BadRequest('Missing user authentication')
user = None
try:
user = AstakosUser.objects.get(auth_token=auth_token)
except:
pass
if not user:
raise BadRequest('Invalid user authentication')
form = FeedbackForm(request.POST)
if not form.is_valid():
raise BadRequest('Invalid data')
msg = form.cleaned_data['feedback_msg']
data = form.cleaned_data['feedback_data']
send_feedback_func(msg, data, user, email_template_name)
response = HttpResponse(status=200)
response['Content-Length'] = len(response.content)
return response
return __send_feedback(request, email_template_name)
......@@ -38,10 +38,12 @@ from time import time, mktime
from django.http import HttpResponse
from django.utils import simplejson as json
from django.views.decorators.csrf import csrf_exempt
from .faults import (
Fault, Unauthorized, InternalServerError, BadRequest, Forbidden)
from . import render_fault
from . import render_fault, __get_uuid_displayname_catalogs, __send_feedback
from astakos.im.models import AstakosUser
from astakos.im.util import epoch
......@@ -115,8 +117,7 @@ def authenticate(request, user=None):
'email': [user.email],
'name': user.realname,
'auth_token_created': epoch(user.auth_token_created),
'auth_token_expires': epoch(user.auth_token_expires),
'has_credits': user.has_credits}
'auth_token_expires': epoch(user.auth_token_expires)}
# append usage data if requested
if request.REQUEST.get('usage', None):
......@@ -132,3 +133,23 @@ def authenticate(request, user=None):
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
@csrf_exempt
@api_method(http_method='POST', token_required=True)
def get_uuid_displayname_catalogs(request, user=None):
# Normal Response Codes: 200
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
return __get_uuid_displayname_catalogs(request)
@csrf_exempt
@api_method(http_method='POST', token_required=True)
def send_feedback(request, email_template_name='im/feedback_mail.txt', user=None):
# Normal Response Codes: 200
# Error Response Codes: internalServerError (500)
# badRequest (400)
# unauthorised (401)
return __send_feedback(request, email_template_name, user)
......@@ -76,7 +76,7 @@ class AuthProvider(object):
module = None
module_active = False
module_enabled = False
one_per_user = False
one_per_user = True
login_prompt = _('Login using ')
primary_login_prompt = _('Login using ')
login_message = None
......@@ -85,6 +85,8 @@ class AuthProvider(object):
remote_logout_url = None
logout_from_provider_text = None
icon_url = None
icon_medium_url = None
method_prompt = None
def get_message(self, msg, **kwargs):
params = kwargs
......@@ -98,8 +100,25 @@ class AuthProvider(object):
def add_url(self):
return reverse(self.login_view)
def __init__(self, user=None):
@property
def provider_details(self):
if self.user:
if self.identifier:
self._provider_details = \
self.user.get_auth_providers().get(module=self.module,
identifier=self.identifier).__dict__
else:
self._provider_details = self.user.get(module=self.module).__dict__
return self._provider_details
def __init__(self, user=None, identifier=None, provider_details=None):
self.user = user
self.identifier = identifier
self._provider_details = None
if provider_details:
self._provider_details = provider_details
for tpl in ['login_prompt', 'login', 'signup_prompt']:
tpl_name = '%s_%s' % (tpl, 'template')
override = self.get_setting(tpl_name)
......@@ -121,7 +140,14 @@ class AuthProvider(object):
if not self.icon_url:
self.icon_url = '%s%s' % (settings.MEDIA_URL, 'im/auth/icons/%s.png' %
self.get_title_display.lower())
self.module.lower())
if not self.icon_medium_url:
self.icon_medium_url = '%s%s' % (settings.MEDIA_URL, 'im/auth/icons-medium/%s.png' %
self.module.lower())
if not self.method_prompt:
self.method_prompt = _('%s login method') % self.get_title_display
def __getattr__(self, key):
if not key.startswith('get_'):
......@@ -150,6 +176,9 @@ class AuthProvider(object):
return getattr(settings, attr, default)
def is_available_for_remove(self):
return self.is_active() and self.get_setting('CAN_REMOVE', True)
def is_available_for_login(self):
""" A user can login using authentication provider"""
return self.is_active() and self.get_setting('CAN_LOGIN',
......@@ -177,8 +206,9 @@ class LocalAuthProvider(AuthProvider):
module = 'local'
title = _('Local password')
description = _('Create a local password for your account')
add_prompt = _('Create a local password for your account')
login_prompt = _('if you already have a username and password')
add_prompt = _('Enable Classic login for your account')
details_tpl = _('Username: %(username)s')
login_prompt = _('Classic login (username/password)')
signup_prompt = _('New to ~okeanos ?')
signup_link_prompt = _('create an account now')
login_view = 'password_change'
......@@ -190,8 +220,6 @@ class LocalAuthProvider(AuthProvider):
login_template = 'im/auth/local_login_form.html'
login_prompt_template = 'im/auth/local_login_prompt.html'
signup_prompt_template = 'im/auth/local_signup_prompt.html'
details_tpl = _('You can login to your account using your'
' %(auth_backend)s password.')
@property
def extra_actions(self):
......@@ -200,27 +228,26 @@ class LocalAuthProvider(AuthProvider):
class ShibbolethAuthProvider(AuthProvider):
module = 'shibboleth'
title = _('Academic account (Shibboleth)')
add_prompt = _('Allows you to login to your account using your academic '
'account')
details_tpl = _('Shibboleth account \'%(identifier)s\' is connected to your '
' account.')
user_title = _('Academic credentials (%(identifier)s)')
primary_login_prompt = _('If you are a student/researcher/faculty you can'
' login using your university-credentials in'
' the following page')
title = _('Academic account')
add_prompt = _('Enable Academic login for your account')
details_tpl = _('Identifier: %(identifier)s')
user_title = _('Academic account (%(identifier)s)')
primary_login_prompt = _('If you are a student, professor or researcher you '
'can login using your academic account.')
login_view = 'astakos.im.target.shibboleth.login'
login_template = 'im/auth/shibboleth_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
logout_from_provider_text = ' at your Academic account (shibboleth)'
logout_from_provider_text = 'Please close all browser windows to complete logout from your Academic account, too.'
method_prompt = _('Academic account')
class TwitterAuthProvider(AuthProvider):
module = 'twitter'
title = _('Twitter')
add_prompt = _('Allows you to login to your account using Twitter')
details_tpl = _('Twitter screen name: %(info_screen_name)s')
add_prompt = _('Enable Twitter login for your account')
details_tpl = _('Username: %(info_screen_name)s')
user_title = _('Twitter (%(info_screen_name)s)')
login_view = 'astakos.im.target.twitter.login'
......@@ -231,8 +258,8 @@ class TwitterAuthProvider(AuthProvider):
class GoogleAuthProvider(AuthProvider):
module = 'google'
title = _('Google')
add_prompt = _('Allows you to login to your account using Google')
details_tpl = _('Google account: %(info_email)s')
add_prompt = _('Enable Google login for your account')
details_tpl = _('Email: %(info_email)s')
user_title = _('Google (%(info_email)s)')
login_view = 'astakos.im.target.google.login'
......@@ -243,18 +270,21 @@ class GoogleAuthProvider(AuthProvider):
class LinkedInAuthProvider(AuthProvider):
module = 'linkedin'
title = _('LinkedIn')
add_prompt = _('Allows you to login to your account using LinkedIn')
add_prompt = _('Enable LinkedIn login for your account')
details_tpl = _('Email: %(info_emailAddress)s')
user_title = _('LinkedIn (%(info_emailAddress)s)')
details_tpl = _('LinkedIn account: %(info_emailAddress)s')
login_view = 'astakos.im.target.linkedin.login'
login_template = 'im/auth/third_party_provider_generic_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
def get_provider(id, user_obj=None, default=None):
def get_provider(id, user_obj=None, default=None, identifier=None, provider_details={}):