Commit 4485031a authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

Merge branch 'devel-0.13' into multipleauthmethods

Conflicts:
	snf-astakos-app/astakos/im/context_processors.py
	snf-astakos-app/astakos/im/forms.py
	snf-astakos-app/astakos/im/models.py
	snf-astakos-app/astakos/im/target/local.py
	snf-astakos-app/astakos/im/target/shibboleth.py
	snf-astakos-app/astakos/im/urls.py
	snf-astakos-app/astakos/im/views.py
parents 4b904cf9 55e9bed8
......@@ -6,3 +6,5 @@ docs/build
.project
.pydevproject
snf-astakos-app/astakos/version.py
snf-astakos-app/distribute-0.6.10-py2.6.egg
snf-astakos-app/distribute-0.6.10.tar.gz
\ No newline at end of file
#Mon Nov 05 16:04:44 EET 2012
eclipse.preferences.version=1
encoding//astakos/im/migrations/0010_auto__add_field_astakosuser_activation_sent__chg_field_service_url.py=utf-8
encoding//astakos/im/migrations/0011_set_old_activation_sent.py=utf-8
encoding//astakos/im/migrations/0017_populate_resource_data.py=utf-8
encoding//astakos/im/migrations/0030_populate_resource_data.py=utf-8
Changelog
---------
next
^^^^
- Setting ASTAKOS_DEFAULT_ADMIN_EMAIL has been deprecated. Use ADMINS django setting instead.
- Setting ASTAKOS_DEFAULT_FROM_EMAIL has been deprecated. Use SERVER_EMAIL django setting instead.
v0.7.5
^^^^^^
......
......@@ -85,10 +85,43 @@ ASTAKOS_INVITATION_EMAIL_SUBJECT 'Invitation to %s alpha2 testing' %
ASTAKOS_GREETING_EMAIL_SUBJECT 'Welcome to %s alpha2 testing' % SITENAME Welcome email subject
ASTAKOS_FEEDBACK_EMAIL_SUBJECT 'Feedback from %s alpha2 testing' % SITENAME Feedback email subject
ASTAKOS_VERIFICATION_EMAIL_SUBJECT '%s alpha2 testing account activation is needed' % SITENAME Account activation email subject
ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT '%s alpha2 testing account created (%%(user)s)' % SITENAME Account creation admin notification email subject
ASTAKOS_ACCOUNT_CREATION_SUBJECT '%s alpha2 testing account created (%%(user)s)' % SITENAME Account creation email subject
ASTAKOS_GROUP_CREATION_SUBJECT '%s alpha2 testing group created (%%(group)s)' % SITENAME Group creation email subject
ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT '%s alpha2 testing account activated (%%(user)s)' % SITENAME Account activation helpdesk notification email subject
ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT 'Email change on %s alpha2 testing' % SITENAME Email change subject
ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT 'Password reset on %s alpha2 testing' % SITENAME Password change email subject
ASTAKOS_QUOTA_HOLDER_URL '' The quota holder URI
e.g. ``http://localhost:8080/api/quotaholder/v``
ASTAKOS_SERVICES {'cyclades': {'resources': [{'desc': 'Number of virtual machines', Default cloud service information
'group': 'compute',
'name': 'vm',
'uplimit': 2},
{'desc': 'Virtual machine disk size',
'group': 'compute',
'name': 'diskspace',
'unit': 'GB',
'uplimit': 5},
{'desc': 'Number of virtual machine processors',
'group': 'compute',
'name': 'cpu',
'uplimit': 1},
{'desc': 'Virtual machines',
'group': 'compute',
'name': 'ram',
'unit': 'MB',
'uplimit': 1024}],
'url': 'https://node1.example.com/ui/'},
'pithos+': {'resources': [{'desc': 'Pithos account diskspace',
'group': 'storage',
'name': 'diskspace',
'unit': 'bytes',
'uplimit': 5368709120}],
'url': 'https://node2.example.com/ui/'}}
ASTAKOS_AQUARIUM_URL '' The billing (aquarium) URI
e.g. ``http://localhost:8888/user``
ASTAKOS_PAGINATE_BY 10 Number of object to be displayed per page
ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN True Enforce token renewal on password change/reset. If set to False, user can optionally decide
whether to renew the token or not.
ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION True Permit local account migration to third party account
......@@ -117,11 +150,4 @@ showuser Show user info
To update user credibility from the billing system (Aquarium), enable the queue, install snf-pithos-tools and use ``pithos-dispatcher``::
pithos-dispatcher --exchange=aquarium --callback=astakos.im.queue.listener.on_creditevent
Load groups:
------------
To set the initial user groups load the followind fixture:
snf-manage loaddata groups
pithos-dispatcher --exchange=aquarium --callback=astakos.im.endpoints.aquarium.consumer.on_creditevent
# Copyright (c) Django Software Foundation and individual contributors.
# 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,
#
# 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
#
# 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.
#
#
# 3. Neither the name of Django nor the names of its contributors may be used
# to endorse or promote products derived from this software without
# specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
......@@ -28,6 +28,7 @@
VERSION = (0, 3, 0, 'alpha', 0)
def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1])
if VERSION[2]:
......
......@@ -33,29 +33,26 @@
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.db import transaction
from urlparse import urljoin
from astakos.im.models import AstakosUser, Invitation
from astakos.im.forms import *
from astakos.im.models import AstakosUser
from astakos.im.util import get_invitation
from astakos.im.functions import send_verification, send_activation, \
send_admin_notification, activate, SendMailError
from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, \
DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
from astakos.im.functions import (
send_activation, send_account_creation_notification, activate
)
from astakos.im.settings import (
INVITATIONS_ENABLED, MODERATION_ENABLED, RE_USER_EMAIL_PATTERNS
)
from astakos.im.forms import *
import astakos.im.messages as astakos_messages
import socket
import logging
import re
logger = logging.getLogger(__name__)
def get_backend(request):
"""
Returns an instance of an activation backend,
......@@ -68,31 +65,36 @@ def get_backend(request):
"""
module = 'astakos.im.activation_backends'
prefix = 'Invitations' if INVITATIONS_ENABLED else 'Simple'
backend_class_name = '%sBackend' %prefix
backend_class_name = '%sBackend' % prefix
try:
mod = import_module(module)
except ImportError, e:
raise ImproperlyConfigured('Error loading activation backend %s: "%s"' % (module, e))
raise ImproperlyConfigured(
'Error loading activation backend %s: "%s"' % (module, e))
try:
backend_class = getattr(mod, backend_class_name)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, backend_class_name))
return backend_class(request)
class ActivationBackend(object):
def __init__(self, request):
self.request = request
def _is_preaccepted(self, user):
# return True if user email matches specific patterns
for pattern in RE_USER_EMAIL_PATTERNS:
if re.match(pattern, user.email):
return True
return False
def get_signup_form(self, provider='local', instance=None):
"""
Returns a form instance of the relevant class
"""
main = provider.capitalize() if provider == 'local' else 'ThirdParty'
suffix = 'UserCreationForm'
suffix = 'UserCreationForm'
formclass = '%s%s' % (main, suffix)
request = self.request
initial_data = None
......@@ -126,12 +128,16 @@ class ActivationBackend(object):
send_activation(user, activation_template_name)
return VerificationSent()
else:
send_admin_notification(user, admin_email_template_name)
send_account_creation_notification(
template_name=admin_email_template_name,
dictionary={'user': user.__dict__, 'group_creation': True}
)
return NotificationSent()
except BaseException, e:
logger.exception(e)
raise e
class InvitationsBackend(ActivationBackend):
"""
A activation backend which implements the following workflow: a user
......@@ -140,14 +146,11 @@ class InvitationsBackend(ActivationBackend):
account is created and the user is going to receive an email as soon as an
administrator activates his/her account.
"""
def __init__(self, request):
self.request = request
super(InvitationsBackend, self).__init__()
def get_signup_form(self, provider='local', instance=None):
"""
Returns a form instance of the relevant class
raises Invitation.DoesNotExist and ValueError if invitation is consumed
or invitation username is reserved.
"""
......@@ -156,7 +159,7 @@ class InvitationsBackend(ActivationBackend):
initial_data = self.get_signup_initial_data(provider)
prefix = 'Invited' if invitation else ''
main = provider.capitalize()
suffix = 'UserCreationForm'
suffix = 'UserCreationForm'
formclass = '%s%s%s' % (prefix, main, suffix)
return globals()[formclass](initial_data, instance=instance, request=self.request)
......@@ -173,12 +176,12 @@ class InvitationsBackend(ActivationBackend):
if invitation:
# create a tmp user with the invitation realname
# to extract first and last name
u = AstakosUser(realname = invitation.realname)
initial_data = {'email':invitation.username,
'inviter':invitation.inviter.realname,
'first_name':u.first_name,
'last_name':u.last_name,
'provider':provider}
u = AstakosUser(realname=invitation.realname)
initial_data = {'email': invitation.username,
'inviter': invitation.inviter.realname,
'first_name': u.first_name,
'last_name': u.last_name,
'provider': provider}
else:
if provider == request.POST.get('provider', ''):
initial_data = request.POST
......@@ -199,16 +202,13 @@ class InvitationsBackend(ActivationBackend):
return True
return False
class SimpleBackend(ActivationBackend):
"""
A activation backend which implements the following workflow: a user
supplies the necessary registation information, an incative user account is
created and receives an email in order to activate his/her account.
"""
def __init__(self, request):
self.request = request
super(SimpleBackend, self).__init__()
def _is_preaccepted(self, user):
if super(SimpleBackend, self)._is_preaccepted(user):
return True
......@@ -216,23 +216,24 @@ class SimpleBackend(ActivationBackend):
return False
return True
class ActivationResult(object):
def __init__(self, message):
self.message = message
class VerificationSent(ActivationResult):
def __init__(self):
message = _('Verification sent.')
message = _(astakos_messages.VERIFICATION_SENT)
super(VerificationSent, self).__init__(message)
class NotificationSent(ActivationResult):
def __init__(self):
message = _('Your request for an account was successfully received and is now pending \
approval. You will be notified by email in the next few days. Thanks for \
your interest in ~okeanos! The GRNET team.')
message = _(astakos_messages.NOTIFICATION_SENT)
super(NotificationSent, self).__init__(message)
class RegistationCompleted(ActivationResult):
def __init__(self):
message = _('Registration completed. You can now login.')
message = _(astakos_messages.REGISTRATION_COMPLETED)
super(RegistationCompleted, self).__init__(message)
\ No newline at end of file
......@@ -31,60 +31,230 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from functools import wraps
from traceback import format_exc
from urllib import quote, unquote
from django.http import HttpResponse
from django.utils import simplejson as json
from django.conf import settings
from django.core.urlresolvers import reverse
from astakos.im.models import AstakosUser, GroupKind, Service, Resource
from astakos.im.api.faults import Fault, ItemNotFound, InternalServerError, BadRequest
from astakos.im.settings import INVITATIONS_ENABLED, COOKIE_NAME, EMAILCHANGE_ENABLED
from astakos.im.models import AstakosUser
from astakos.im.api.faults import ItemNotFound
import logging
logger = logging.getLogger(__name__)
format = ('%a, %d %b %Y %H:%M:%S GMT')
absolute = lambda request, url: request.build_absolute_uri(url)
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:
data += '\n' + fault.details
response = HttpResponse(data, status=fault.code)
response['Content-Length'] = len(response.content)
return response
def api_method(http_method=None):
"""Decorator function for views that implement an API method."""
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
try:
if http_method and request.method != http_method:
raise BadRequest('Method not allowed.')
response = func(request, *args, **kwargs)
return response
except Fault, fault:
return render_fault(request, fault)
except BaseException, e:
logger.exception('Unexpected error: %s' % e)
fault = InternalServerError('Unexpected error')
return render_fault(request, fault)
return wrapper
return decorator
def _get_user_by_username(user_id):
try:
user = AstakosUser.objects.get(username = user_id)
except AstakosUser.DoesNotExist, e:
user = AstakosUser.objects.get(username=user_id)
except AstakosUser.DoesNotExist:
raise ItemNotFound('Invalid userid')
else:
response = HttpResponse()
response.status=200
user_info = {'id':user.id,
'username':user.username,
'email':[user.email],
'name':user.realname,
'auth_token_created':user.auth_token_created.strftime(format),
'auth_token_expires':user.auth_token_expires.strftime(format),
'has_credits':user.has_credits,
'enabled':user.is_active,
'groups':[g.name for g in user.groups.all()]}
response.status = 200
user_info = {'id': user.id,
'username': user.username,
'email': [user.email],
'name': user.realname,
'auth_token_created': user.auth_token_created.strftime(format),
'auth_token_expires': user.auth_token_expires.strftime(format),
'has_credits': user.has_credits,
'enabled': user.is_active,
'groups': [g.name for g in user.groups.all()]}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
def _get_user_by_email(email):
if not email:
raise BadRequest('Email missing')
try:
user = AstakosUser.objects.get(email = email)
except AstakosUser.DoesNotExist, e:
user = AstakosUser.objects.get(email__iexact=email)
except AstakosUser.DoesNotExist:
raise ItemNotFound('Invalid email')
if not user.is_active:
raise ItemNotFound('Inactive user')
else:
response = HttpResponse()
response.status=200
user_info = {'id':user.id,
'username':user.username,
'email':[user.email],
'enabled':user.is_active,
'name':user.realname,
'auth_token_created':user.auth_token_created.strftime(format),
'auth_token_expires':user.auth_token_expires.strftime(format),
'has_credits':user.has_credits,
'groups':[g.name for g in user.groups.all()],
'user_permissions':[p.codename for p in user.user_permissions.all()]}
response.status = 200
user_info = {'id': user.id,
'username': user.username,
'email': [user.email],
'enabled': user.is_active,
'name': user.realname,
'auth_token_created': user.auth_token_created.strftime(format),
'auth_token_expires': user.auth_token_expires.strftime(format),
'has_credits': user.has_credits,
'groups': [g.name for g in user.groups.all()],
'user_permissions': [p.codename for p in user.user_permissions.all()]}
response.content = json.dumps(user_info)
response['Content-Type'] = 'application/json; charset=UTF-8'
response['Content-Length'] = len(response.content)
return response
\ No newline at end of file
return response
@api_method(http_method='GET')
def get_services(request):
callback = request.GET.get('callback', None)
services = Service.objects.all()
data = tuple({'id': s.pk, 'name': s.name, 'url': s.url, 'icon':
s.icon} for s in services)
data = json.dumps(data)
mimetype = 'application/json'
if callback:
mimetype = 'application/javascript'
data = '%s(%s)' % (callback, data)
return HttpResponse(content=data, mimetype=mimetype)
@api_method()
def get_menu(request, with_extra_links=False, with_signout=True):
user = request.user
index_url = reverse('index')
l = [{'url': absolute(request, index_url), 'name': "Sign in"}]
if user.is_authenticated():
l = []
append = l.append
item = MenuItem
item.current_path = absolute(request, request.path)
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 user.has_usable_password() and user.provider in ('local', ''):
append(item(
url=absolute(request, reverse('password_change')),
name="Change password"))
if EMAILCHANGE_ENABLED:
append(item(
url=absolute(request, reverse('email_change')),
name="Change email"))
if INVITATIONS_ENABLED:
append(item(
url=absolute(request, reverse('invite')),
name="Invitations"))
append(item(
url=absolute(request, reverse('group_list')),
name="Projects",
submenu=(item(
url=absolute(request,
reverse('group_list')),
name="Overview"),
item(
url=absolute(request,
reverse('group_create_list')),
name="Create"),
item(
url=absolute(request,
reverse('group_search')),
name="Join"),)))
append(item(
url=absolute(request, reverse('resource_list')),
name="Report"))
append(item(
url=absolute(request, reverse('feedback')),
name="Feedback"))
append(item(
url=absolute(request, reverse('billing')),
name="Billing"))
append(item(
url=absolute(request, reverse('timeline')),
name="Timeline"))
if with_signout:
append(item(
url=absolute(request, reverse('logout')),
name="Sign out"))
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)
class MenuItem(dict):
current_path = ''
def __init__(self, *args, **kwargs):
super(MenuItem, self).__init__(*args, **kwargs)
if kwargs.get('url') or kwargs.get('submenu'):
self.__set_is_active__()
def __setitem__(self, key, value):
super(MenuItem, self).__setitem__(key, value)
if key in ('url', 'submenu'):
self.__set_is_active__()
def __set_is_active__(self):
if self.get('is_active'):
return
if self.current_path == self.get('url'):
self.__setitem__('is_active', True)
else:
submenu = self.get('submenu', ())
current = (i for i in submenu if i.get('url') == self.current_path)
try:
current_node = current.next()
if not current_node.get('is_active'):
current_node.__setitem__('is_active', True)
self.__setitem__('is_active', True)
except StopIteration:
return
def __setattribute__(self, name, value):
super(MenuItem, self).__setattribute__(name, value)
if name == 'current_path':
self.__set_is_active__()
......@@ -32,40 +32,24 @@
# or implied, of GRNET S.A.
import logging
import urllib
from functools import wraps
from traceback import format_exc
from time import time, mktime
from urllib import quote
from urlparse import urlparse
from collections import defaultdict
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.api.faults import *
from astakos.im.models import AstakosUser, Service
from astakos.im.settings import INVITATIONS_ENABLED, EMAILCHANGE_ENABLED
from astakos.im.api.faults import (
Fault, Unauthorized, InternalServerError, BadRequest,
Forbidden
)