Commit 9cbf88dc authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

Merge branch 'master', remote-tracking branch 'origin' into aquarium

parents 201ee3ae ee1de5e9
Administrator Guide
===================
Simple Setup
------------
Assuming a clean debian squeeze (stable) installation, use the following steps to run the software.
Install packages::
apt-get install git python-django python-django-south python-setuptools python-sphinx python-httplib2
apt-get install apache2 libapache2-mod-wsgi
Get the source::
cd /
git clone https://code.grnet.gr/git/astakos
Setup the files::
cd /astakos/astakos
python manage.py syncdb (At this point you will prompt to create a superuser)
python manage.py migrate im 0001 --fake
python manage.py migrate im
python loaddata im/fixtures/admin_user.json (Load additional information for the newly created superuser)
cd /astakos
python setup.py build_sphinx
python manage runserver
It is advised that you create a ``settings.local`` file to place any configuration overrides (at least change ``SECRET_KEY``).
Twitter Setup
-------------
......@@ -12,7 +12,7 @@ Users in astakos can be authenticated via several identity providers:
* Twitter
* Shibboleth
It provides also an administrative interface for managing user accounts.
It provides also a command line tool for managing user accounts.
It is build over django and extends its authentication mechanism.
......@@ -102,7 +102,7 @@ Logged on users can perform a number of actions:
* send feedback for grnet services via: ``/im/send_feedback``
* logout (and delete cookie) via: ``/im/logout``
User entries can also be modified/added via the administrative interface available at ``/im/admin``.
User entries can also be modified/added via the ``snf-manage activateuser`` command.
A superuser account can be created the first time you run the ``manage.py syncdb`` django command and then loading the extra user data from the ``admin_user`` fixture. At a later date, the ``manage.py createsuperuser`` command line utility can be used (as long as the extra user data for Astakos is added with a fixture or by hand).
......@@ -126,15 +126,15 @@ Finally, backend systems having acquired a token can use the :ref:`authenticate-
The Astakos API
---------------
All 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`)
.. _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
Uri Method Description
==================== ========= ==================
``/im/authenticate`` GET Authenticate user using token
==================== ========= ==================
......@@ -179,3 +179,59 @@ Return Code Description
401 (Unauthorized) Missing token or inactive user
500 (Internal Server Error) The request cannot be completed because of an internal error
=========================== =====================
Get Services
^^^^^^^^^^^^
Returns a json formatted list containing information about the supported cloud services.
==================== ========= ==================
Uri Method Description
==================== ========= ==================
``/im/get_services`` GET Get cloud services
==================== ========= ==================
Example reply:
::
[{"url": "/", "icon": "home-icon.png", "name": "grnet cloud", "id": "cloud"},
{"url": "/okeanos.html", "name": "~okeanos", "id": "okeanos"},
{"url": "/ui/", "name": "pithos+", "id": "pithos"}]
Get Menu
^^^^^^^^
Returns a json formatted list containing the cloud bar links.
==================== ========= ==================
Uri Method Description
==================== ========= ==================
``/im/get_menu`` GET Get cloud bar menu
==================== ========= ==================
|
====================== =========================
Request Parameter Name Value
====================== =========================
location Location to pass in the next parameter
====================== =========================
Example reply if request user is not authenticated:
::
[{"url": "/im/login?next=", "name": "login..."}]
Example reply if request user is authenticated:
[{"url": "/im/profile", "name": "spapagian@grnet.gr"},
{"url": "/im/profile", "name": "view your profile..."},
{"url": "/im/password", "name": "change your password..."},
{"url": "/im/feedback", "name": "feedback..."},
{"url": "/im/logout", "name": "logout..."}]
......@@ -7,7 +7,6 @@ Contents:
:maxdepth: 3
devguide
adminguide
views
models
forms
......
v0.3.1
======
- bug fixes
- administrator email notification on sign up
(configurable from ASTAKOS_DEFAULT_ADMIN_EMAIL setting)
- recaptcha integration on sign up form
(requires ASTAKOS_RECAPTCHA_PUBLIC_KEY, ASTAKOS_RECAPTCHA_PRIVATE_KEY
settings)
- cloudbar jsonp responses
......@@ -40,27 +40,33 @@ Settings
Configure in ``settings.py`` or a ``.conf`` file in ``/etc/synnefo`` if using snf-webproject.
=============================== ================================================ ============================================================
Name Default value Description
=============================== ================================================ ============================================================
ASTAKOS_AUTH_TOKEN_DURATION one month Expiration time of newly created auth tokens
ASTAKOS_TWITTER_KEY '' Twitter ``oauth_token``
ASTAKOS_TWITTER_SECRET '' Twitter ``oauth_token_secret``
ASTAKOS_DEFAULT_USER_LEVEL 4 Default (not-invited) user level
ASTAKOS_INVITATIONS_PER_LEVEL {0:100, 1:2, 2:0, 3:0, 4:0} Number of user invitations per user level
ASTAKOS_DEFAULT_FROM_EMAIL GRNET Cloud <no-reply@grnet.gr> ``from`` parameter passed in ``django.core.mail.send_mail``
ASTAKOS_DEFAULT_CONTACT_EMAIL support@cloud.grnet.gr Contact email
ASTAKOS_IM_MODULES ['local', 'twitter', 'shibboleth']) Signup modules
ASTAKOS_FORCE_PROFILE_UPDATE True Force user profile verification
ASTAKOS_INVITATIONS_ENABLED True Enable invitations
ASTAKOS_COOKIE_NAME '_pithos2_a' ``Key`` parameter passed in ``django.http.HttpResponse.set_cookie``
ASTAKOS_COOKIE_DOMAIN None ``Domain`` parameter passed in ``django.http.HttpResponse.set_cookie``
ASTAKOS_COOKIE_SECURE True ``Secure`` parameter passed in ``django.http.HttpResponse.set_cookie``
ASTAKOS_IM_STATIC_URL '/static/im/' URL to use when referring to static files
ASTAKOS_MODERATION_ENABLED True If False and invitations are not enabled newly created user will be automatically accepted
ASTAKOS_BASEURL 'http://pithos.dev.grnet.gr' Astakos baseurl
ASTAKOS_SITENAME 'GRNET Cloud' Service name that appears in emails
=============================== ================================================ ============================================================
============================== ============================================================================= ===========================================================================================
Name Default value Description
============================== ============================================================================= ===========================================================================================
ASTAKOS_AUTH_TOKEN_DURATION one month Expiration time of newly created auth tokens
ASTAKOS_TWITTER_KEY Twitter ``oauth_token``
ASTAKOS_TWITTER_SECRET Twitter ``oauth_token_secret``
ASTAKOS_DEFAULT_USER_LEVEL 4 Default (not-invited) user level
ASTAKOS_INVITATIONS_PER_LEVEL {0:100, 1:2, 2:0, 3:0, 4:0} Number of user invitations per user level
ASTAKOS_DEFAULT_FROM_EMAIL GRNET Cloud <no-reply\@grnet.gr> ``from`` parameter passed in ``django.core.mail.send_mail``
ASTAKOS_DEFAULT_CONTACT_EMAIL support\@cloud.grnet.gr Contact email
ASTAKOS_DEFAULT_ADMIN_EMAIL support\@cloud.grnet.gr Administrator email to receive user creation notifications (if None disables notifications)
ASTAKOS_IM_MODULES ['local', 'twitter', 'shibboleth'] Signup modules
ASTAKOS_FORCE_PROFILE_UPDATE True Force user profile verification
ASTAKOS_INVITATIONS_ENABLED True Enable invitations
ASTAKOS_COOKIE_NAME _pithos2_a ``Key`` parameter passed in ``django.http.HttpResponse.set_cookie``
ASTAKOS_COOKIE_DOMAIN None ``Domain`` parameter passed in ``django.http.HttpResponse.set_cookie``
ASTAKOS_COOKIE_SECURE True ``Secure`` parameter passed in ``django.http.HttpResponse.set_cookie``
ASTAKOS_IM_STATIC_URL /static/im/ URL to use when referring to static files
ASTAKOS_MODERATION_ENABLED True If False and invitations are not enabled newly created user will be automatically accepted
ASTAKOS_BASEURL \http://pithos.dev.grnet.gr Astakos baseurl
ASTAKOS_SITENAME GRNET Cloud Service name that appears in emails
ASTAKOS_CLOUD_SERVICES ({'icon': 'home-icon.png', 'id': 'cloud', 'name': 'grnet cloud', 'url': '/'}, Cloud services appear in the horizontal bar
{'id': 'okeanos', 'name': '~okeanos', 'url': '/okeanos.html'},
{'id': 'pithos', 'name': 'pithos+', 'url': '/ui/'})
ASTAKOS_RECAPTCHA_PUBLIC_KEY Recaptcha public key obtained after registration here: http://recaptcha.net
ASTAKOS_RECAPTCHA_PRIVATE_KEY Recaptcha private key obtained after registration here: http://recaptcha.net
============================== ============================================================================= ===========================================================================================
Administrator functions
-----------------------
......
# Copyright 2011-2012 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
......@@ -25,7 +25,7 @@
# 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
......@@ -33,17 +33,22 @@
from traceback import format_exc
from time import time, mktime
from urllib import quote
from urlparse import urlparse
from django.conf import settings
from django.http import HttpResponse
from django.utils import simplejson as json
from django.core.urlresolvers import reverse
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError
from astakos.im.models import AstakosUser
from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED
def render_fault(request, fault):
if isinstance(fault, InternalServerError) and settings.DEBUG:
fault.details = format_exc(fault)
request.serialization = 'text'
data = fault.message + '\n'
if fault.details:
......@@ -63,20 +68,20 @@ def authenticate(request):
x_auth_token = request.META.get('HTTP_X_AUTH_TOKEN')
if not x_auth_token:
return render_fault(request, BadRequest('Missing X-Auth-Token'))
try:
user = AstakosUser.objects.get(auth_token=x_auth_token)
except AstakosUser.DoesNotExist, e:
return render_fault(request, Unauthorized('Invalid X-Auth-Token'))
return render_fault(request, Unauthorized('Invalid X-Auth-Token'))
# Check if the is active.
if not user.is_active:
return render_fault(request, Unauthorized('User inactive'))
# Check if the token has expired.
if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
return render_fault(request, Unauthorized('Authentication expired'))
response = HttpResponse()
response.status=204
user_info = {'username':user.username,
......@@ -91,3 +96,53 @@ def authenticate(request):
except BaseException, e:
fault = InternalServerError('Unexpected error')
return render_fault(request, fault)
def get_services(request):
if request.method != 'GET':
raise BadRequest('Method not allowed.')
callback = request.GET.get('callback', None)
data = json.dumps(CLOUD_SERVICES)
mimetype = 'application/json'
if callback:
mimetype = 'application/javascript'
data = '%s(%s)' % (callback, data)
return HttpResponse(content=data, mimetype=mimetype)
def get_menu(request):
if request.method != 'GET':
raise BadRequest('Method not allowed.')
location = request.GET.get('location', '')
absolute = lambda (url): request.build_absolute_uri(url)
index_url = absolute(reverse('astakos.im.views.index'))
if urlparse(location).query.rfind('next=') == -1:
index_url = '%s?next=%s' % (index_url, quote(location))
l = [{ 'url': index_url, 'name': "login..."}]
if request.user.is_authenticated():
l = []
l.append({ 'url': absolute(reverse('astakos.im.views.edit_profile')),
'name': request.user.email})
l.append({ 'url': absolute(reverse('astakos.im.views.edit_profile')),
'name': "view your profile..." })
if request.user.password:
l.append({ 'url': absolute(reverse('password_change')),
'name': "change your password..." })
if INVITATIONS_ENABLED:
l.append({ 'url': absolute(reverse('astakos.im.views.invite')),
'name': "invite some friends..." })
l.append({ 'url': absolute(reverse('astakos.im.views.send_feedback')),
'name': "feedback..." })
l.append({ 'url': absolute(reverse('astakos.im.views.logout')),
'name': "logout..."})
callback = request.GET.get('callback', None)
data = json.dumps(tuple(l))
mimetype = 'application/json'
if callback:
mimetype = 'application/javascript'
data = '%s(%s)' % (callback, data)
return HttpResponse(content=data, mimetype=mimetype)
......@@ -48,7 +48,7 @@ from urlparse import urljoin
from astakos.im.models import AstakosUser, Invitation
from astakos.im.forms import *
from astakos.im.util import get_invitation
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL
import socket
import logging
......@@ -61,7 +61,7 @@ def get_backend(request):
according to the INVITATIONS_ENABLED setting
(if True returns ``astakos.im.backends.InvitationsBackend`` and if False
returns ``astakos.im.backends.SimpleBackend``).
If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
is raised.
"""
......@@ -93,10 +93,10 @@ class InvitationsBackend(object):
"""
self.request = request
self.invitation = get_invitation(request)
def get_signup_form(self, provider):
"""
Returns the form class name
Returns the form class name
"""
invitation = self.invitation
initial_data = self.get_signup_initial_data(provider)
......@@ -104,12 +104,14 @@ class InvitationsBackend(object):
main = provider.capitalize() if provider == 'local' else 'ThirdParty'
suffix = 'UserCreationForm'
formclass = '%s%s%s' % (prefix, main, suffix)
return globals()[formclass](initial_data)
ip = self.request.META.get('REMOTE_ADDR',
self.request.META.get('HTTP_X_REAL_IP', None))
return globals()[formclass](initial_data, ip=ip)
def get_signup_initial_data(self, provider):
"""
Returns the necassary registration form depending the user is invited or not
Throws Invitation.DoesNotExist in case ``code`` is not valid.
"""
request = self.request
......@@ -128,7 +130,7 @@ class InvitationsBackend(object):
if provider == request.POST.get('provider', ''):
initial_data = request.POST
return initial_data
def _is_preaccepted(self, user):
"""
If there is a valid, not-consumed invitation code for the specific user
......@@ -141,15 +143,15 @@ class InvitationsBackend(object):
invitation.consume()
return True
return False
@transaction.commit_manually
def signup(self, form):
def signup(self, form, admin_email_template_name='im/admin_notification.txt'):
"""
Initially creates an inactive user account. If the user is preaccepted
(has a valid invitation code) the user is activated and if the request
param ``next`` is present redirects to it.
In any other case the method returns the action status and a message.
The method uses commit_manually decorator in order to ensure the user
will be created only if the procedure has been completed successfully.
"""
......@@ -161,6 +163,7 @@ class InvitationsBackend(object):
user.save()
message = _('Registration completed. You can now login.')
else:
_send_notification(user, admin_email_template_name)
message = _('Registration completed. You will receive an email upon your account\'s activation.')
status = messages.SUCCESS
except Invitation.DoesNotExist, e:
......@@ -169,7 +172,7 @@ class InvitationsBackend(object):
except socket.error, e:
status = messages.ERROR
message = _(e.strerror)
# rollback in case of error
if status == messages.ERROR:
transaction.rollback()
......@@ -185,7 +188,7 @@ class SimpleBackend(object):
"""
def __init__(self, request):
self.request = request
def get_signup_form(self, provider):
"""
Returns the form class name
......@@ -198,35 +201,43 @@ class SimpleBackend(object):
if request.method == 'POST':
if provider == request.POST.get('provider', ''):
initial_data = request.POST
return globals()[formclass](initial_data)
ip = self.request.META.get('REMOTE_ADDR',
self.request.META.get('HTTP_X_REAL_IP', None))
return globals()[formclass](initial_data, ip=ip)
@transaction.commit_manually
def signup(self, form, email_template_name='im/activation_email.txt'):
def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
"""
Creates an inactive user account and sends a verification email.
The method uses commit_manually decorator in order to ensure the user
will be created only if the procedure has been completed successfully.
** Arguments **
``email_template_name``
A custom template for the verification email body to use. This is
optional; if not specified, this will default to
``im/activation_email.txt``.
** Templates **
im/activation_email.txt or ``email_template_name`` keyword argument
** Settings **
* DEFAULT_CONTACT_EMAIL: service support email
* DEFAULT_FROM_EMAIL: from email
"""
user = form.save()
status = messages.SUCCESS
if MODERATION_ENABLED:
message = _('Registration completed. You will receive an email upon your account\'s activation.')
try:
_send_notification(user, admin_email_template_name)
message = _('Registration completed. You will receive an email upon your account\'s activation.')
except (SMTPException, socket.error) as e:
status = messages.ERROR
name = 'strerror'
message = getattr(e, name) if hasattr(e, name) else e
else:
try:
_send_verification(self.request, user, email_template_name)
......@@ -235,7 +246,7 @@ class SimpleBackend(object):
status = messages.ERROR
name = 'strerror'
message = getattr(e, name) if hasattr(e, name) else e
# rollback in case of error
if status == messages.ERROR:
transaction.rollback()
......@@ -256,3 +267,15 @@ def _send_verification(request, user, template_name):
sender = DEFAULT_FROM_EMAIL
send_mail('%s account activation' % SITENAME, message, sender, [user.email])
logger.info('Sent activation %s', user)
def _send_notification(user, template_name):
if not DEFAULT_ADMIN_EMAIL:
return
message = render_to_string(template_name, {
'user': user,
'baseurl': BASEURL,
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
sender = DEFAULT_FROM_EMAIL
send_mail('%s account notification' % SITENAME, message, sender, [DEFAULT_ADMIN_EMAIL])
logger.info('Sent admin notification for user %s', user)
......@@ -34,6 +34,7 @@
from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL, \
COOKIE_NAME
from django.conf import settings
from django.core.urlresolvers import reverse
def im_modules(request):
return {'im_modules': IM_MODULES}
......@@ -57,8 +58,11 @@ def cloudbar(request):
CB_LOCATION = getattr(settings, 'CLOUDBAR_LOCATION', IM_STATIC_URL + 'cloudbar/')
CB_COOKIE_NAME = getattr(settings, 'CLOUDBAR_COOKIE_NAME', COOKIE_NAME)
CB_ACTIVE_SERVICE = getattr(settings, 'CLOUDBAR_ACTIVE_SERVICE', 'cloud')
absolute = lambda (url): request.build_absolute_uri(url)
return {'CLOUDBAR_LOC': CB_LOCATION,
'CLOUDBAR_COOKIE_NAME': CB_COOKIE_NAME,
'ACTIVE_SERVICE': CB_ACTIVE_SERVICE}
'ACTIVE_SERVICE': CB_ACTIVE_SERVICE,
'GET_SERVICES_URL': absolute(reverse('astakos.im.api.get_services')),
'GET_MENU_URL': absolute(reverse('astakos.im.api.get_menu'))}
......@@ -42,9 +42,11 @@ from django.utils.http import int_to_base36
from django.core.urlresolvers import reverse
from astakos.im.models import AstakosUser
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY
from astakos.im.widgets import DummyWidget, RecaptchaWidget
import logging
import recaptcha.client.captcha as captcha
logger = logging.getLogger(__name__)
......@@ -56,6 +58,8 @@ class LocalUserCreationForm(UserCreationForm):
* The username field isn't visible and it is assigned a generated id.
* User created is not active.
"""
recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
class Meta:
model = AstakosUser
......@@ -65,9 +69,14 @@ class LocalUserCreationForm(UserCreationForm):
"""
Changes the order of fields, and removes the username field.
"""
if 'ip' in kwargs:
self.ip = kwargs['ip']
kwargs.pop('ip')
super(LocalUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'password1', 'password2']
'password1', 'password2',
'recaptcha_challenge_field',
'recaptcha_response_field']
def clean_email(self):
email = self.cleaned_data['email']
......@@ -79,6 +88,23 @@ class LocalUserCreationForm(UserCreationForm):
except AstakosUser.DoesNotExist:
return email
def clean_recaptcha_response_field(self):
if 'recaptcha_challenge_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_response_field']
def clean_recaptcha_challenge_field(self):
if 'recaptcha_response_field' in self.cleaned_data:
self.validate_captcha()
return self.cleaned_data['recaptcha_challenge_field']
def validate_captcha(self):
rcf = self.cleaned_data['recaptcha_challenge_field']
rrf = self.cleaned_data['recaptcha_response_field']
check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
if not check.is_valid:
raise forms.ValidationError(_('You have not entered the correct words'))
def save(self, commit=True):
"""
Saves the email, first_name and last_name properties, after the normal
......@@ -163,6 +189,9 @@ class ThirdPartyUserCreationForm(ProfileForm):
fields = ('email', 'last_name', 'first_name', 'affiliation', 'provider', 'third_party_identifier')
def __init__(self, *args, **kwargs):
if 'ip' in kwargs:
self.ip = kwargs['ip']
kwargs.pop('ip')
super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email']
......
......@@ -21,6 +21,7 @@ INVITATIONS_PER_LEVEL = getattr(settings, 'ASTAKOS_INVITATIONS_PER_LEVEL', {
# Address to use for outgoing emails
DEFAULT_FROM_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_FROM_EMAIL', 'GRNET Cloud <no-reply@grnet.gr>')
DEFAULT_CONTACT_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_CONTACT_EMAIL', 'support@cloud.grnet.gr')
DEFAULT_ADMIN_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_ADMIN_EMAIL', 'support@cloud.grnet.gr')
# Identity Management enabled modules
IM_MODULES = getattr(settings, 'ASTAKOS_IM_MODULES', ['local', 'twitter', 'shibboleth'])
......@@ -45,3 +46,15 @@ BASEURL = getattr(settings, 'ASTAKOS_BASEURL', 'http://pithos.dev.grnet.gr')
# Set service name
SITENAME = getattr(settings