Required auth providers functionality
authorKostas Papadimitriou <kpap@grnet.gr>
Tue, 18 Dec 2012 14:53:31 +0000 (16:53 +0200)
committerKostas Papadimitriou <kpap@grnet.gr>
Tue, 18 Dec 2012 17:53:29 +0000 (19:53 +0200)
if one of auth providers is set to be required, user with no such
provider can only view his profile page and is prompted to add a
new login method.

snf-astakos-app/astakos/im/auth_providers.py
snf-astakos-app/astakos/im/messages.py
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/target/twitter.py
snf-astakos-app/astakos/im/views.py

index cb8ef08..640a824 100644 (file)
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
 
 # providers registry
 PROVIDERS = {}
+REQUIRED_PROVIDERS = {}
 
 class AuthProviderBase(type):
 
@@ -62,6 +63,8 @@ class AuthProviderBase(type):
         newcls = super(AuthProviderBase, cls).__new__(cls, name, bases, dct)
         if include:
             PROVIDERS[type_id] = newcls
+            if newcls().is_required():
+                REQUIRED_PROVIDERS[type_id] = newcls
         return newcls
 
 
@@ -122,6 +125,10 @@ class AuthProvider(object):
         return self.is_active() and self.get_setting('CAN_ADD',
                                                    self.is_active())
 
+    def is_required(self):
+        """Provider required (user cannot remove the last one)"""
+        return self.is_active() and self.get_setting('REQUIRED', False)
+
     def is_active(self):
         return self.module in astakos_settings.IM_MODULES
 
index 3609572..8eb03c5 100644 (file)
@@ -46,6 +46,7 @@ ACCOUNT_PENDING_ACTIVATION_HELP         =   'If you haven\'t received activation
 ACCOUNT_ACTIVATED                       =   'Congratulations. Your account has' + \
                                             ' been activated and you have been' + \
                                             ' automatically signed in to your account.'
+ALREADY_LOGGED_IN                       =   'You are already signed in to your account.'
 PASSWORD_RESET_DONE                     =   'A mail with details on how to change your password was sent.'
 PASSWORD_RESET_CONFIRM_DONE             =   'Password changed. You can now login using your new password.'
 
@@ -156,6 +157,7 @@ AUTH_PROVIDER_ADD_FAILED                     =   "Failed to add new login method
 AUTH_PROVIDER_ADD_EXISTS                     =   "Account already assigned to another user."
 AUTH_PROVIDER_LOGIN_TO_ADD                   =   "The new login method will be assigned once you login to your account."
 AUTH_PROVIDER_INVALID_LOGIN                  =   "No account exists."
+AUTH_PROVIDER_REQUIRED                       =   "%(provider)s login method is required. Add one from your profile page."
 
 
 messages = locals().keys()
index cbd568b..30bb4d4 100644 (file)
@@ -641,14 +641,29 @@ class AstakosUser(User):
 
         return True
 
-    def can_remove_auth_provider(self, provider):
-        if len(self.get_active_auth_providers()) <= 1:
+    def can_remove_auth_provider(self, module):
+        provider = auth_providers.get_provider(module)
+        existing = self.get_active_auth_providers()
+        existing_for_provider = self.get_active_auth_providers(module=module)
+
+        if len(existing) <= 1:
+            return False
+
+        if len(existing_for_provider) == 1 and provider.is_required():
             return False
+
         return True
 
     def can_change_password(self):
         return self.has_auth_provider('local', auth_backend='astakos')
 
+    def has_required_auth_providers(self):
+        required = auth_providers.REQUIRED_PROVIDERS
+        for provider in required:
+            if not self.has_auth_provider(provider):
+                return False
+        return True
+
     def has_auth_provider(self, provider, **kwargs):
         return bool(self.auth_providers.filter(module=provider,
                                                **kwargs).count())
@@ -723,9 +738,9 @@ class AstakosUser(User):
 
         return providers
 
-    def get_active_auth_providers(self):
+    def get_active_auth_providers(self, **filters):
         providers = []
-        for provider in self.auth_providers.active():
+        for provider in self.auth_providers.active(**filters):
             if auth_providers.get_provider(provider.module).is_available_for_login():
                 providers.append(provider)
         return providers
@@ -764,8 +779,8 @@ class AstakosUser(User):
 
 class AstakosUserAuthProviderManager(models.Manager):
 
-    def active(self):
-        return self.filter(active=True)
+    def active(self, **filters):
+        return self.filter(active=True, **filters)
 
 
 class AstakosUserAuthProvider(models.Model):
index 1ac66aa..58c1252 100644 (file)
@@ -47,7 +47,7 @@ from urlparse import urlunsplit, urlsplit
 
 from astakos.im.util import prepare_response, get_context
 from astakos.im.views import requires_anonymous, render_response, \
-        requires_auth_provider
+        requires_auth_provider, required_auth_methods_assigned
 from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
 from astakos.im.models import AstakosUser, PendingThirdPartyUser
 from astakos.im.forms import LoginForm
index 35a8b2d..80fc04e 100644 (file)
@@ -151,7 +151,7 @@ def requires_anonymous(func):
 
 def signed_terms_required(func):
     """
-    Decorator checkes whether the request.user is Anonymous and in that case
+    Decorator checks whether the request.user is Anonymous and in that case
     redirects to `logout`.
     """
     @wraps(func)
@@ -165,6 +165,38 @@ def signed_terms_required(func):
     return wrapper
 
 
+def required_auth_methods_assigned(only_warn=False):
+    """
+    Decorator that checks whether the request.user has all required auth providers
+    assigned.
+    """
+    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()
+
+    def decorator(func):
+        if not required_providers:
+            return func
+
+        @wraps(func)
+        def wrapper(request, *args, **kwargs):
+            if request.user.is_authenticated():
+                for required in required_providers:
+                    if not request.user.has_auth_provider(required):
+                        provider = auth_providers.get_provider(required)
+                        if only_warn:
+                            messages.error(request,
+                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
+                                               'provider': provider.get_title_display}))
+                        else:
+                            return HttpResponseRedirect(reverse('edit_profile'))
+            return func(request, *args, **kwargs)
+        return wrapper
+    return decorator
+
+
+def valid_astakos_user_required(func):
+    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))
+
+
 @require_http_methods(["GET", "POST"])
 @signed_terms_required
 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
@@ -202,8 +234,7 @@ def index(request, login_template_name='im/login.html', profile_template_name='i
 
 
 @require_http_methods(["GET", "POST"])
-@login_required
-@signed_terms_required
+@valid_astakos_user_required
 @transaction.commit_manually
 def invite(request, template_name='im/invitations.html', extra_context=None):
     """
@@ -281,6 +312,7 @@ def invite(request, template_name='im/invitations.html', extra_context=None):
 
 
 @require_http_methods(["GET", "POST"])
+@required_auth_methods_assigned(only_warn=True)
 @login_required
 @signed_terms_required
 def edit_profile(request, template_name='im/profile.html', extra_context=None):
@@ -478,6 +510,7 @@ def signup(request, template_name='im/signup.html', on_success='im/signup_comple
 
 
 @require_http_methods(["GET", "POST"])
+@required_auth_methods_assigned(only_warn=True)
 @login_required
 @signed_terms_required
 def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
@@ -653,8 +686,7 @@ def approval_terms(request, term_id=None, template_name='im/approval_terms.html'
 
 
 @require_http_methods(["GET", "POST"])
-@login_required
-@signed_terms_required
+@valid_astakos_user_required
 @transaction.commit_manually
 def change_email(request, activation_key=None,
                  email_template_name='registration/email_change_email.txt',
@@ -708,6 +740,10 @@ def change_email(request, activation_key=None,
 
 def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
 
+    if request.user.is_authenticated():
+        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
+        return HttpResponseRedirect(reverse('edit_profile'))
+
     if settings.MODERATION_ENABLED:
         raise PermissionDenied
 
@@ -814,8 +850,7 @@ class ResourcePresentation():
 
 
 @require_http_methods(["GET", "POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_add(request, kind_name='default'):
 
     result = callpoint.list_resources()
@@ -880,8 +915,7 @@ def group_add(request, kind_name='default'):
 
 #@require_http_methods(["POST"])
 @require_http_methods(["GET", "POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_add_complete(request):
     model = AstakosGroup
     form = AstakosGroupCreationSummaryForm(request.POST)
@@ -924,8 +958,7 @@ def group_add_complete(request):
 
 #@require_http_methods(["GET"])
 @require_http_methods(["GET", "POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_list(request):
     none = request.user.astakos_groups.none()
     query = """
@@ -986,8 +1019,7 @@ def group_list(request):
 
 
 @require_http_methods(["GET", "POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_detail(request, group_id):
     q = AstakosGroup.objects.select_related().filter(pk=group_id)
     q = q.extra(select={
@@ -1084,8 +1116,7 @@ def group_detail(request, group_id):
 
 
 @require_http_methods(["GET", "POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_search(request, extra_context=None, **kwargs):
     q = request.GET.get('q')
     if request.method == 'GET':
@@ -1160,8 +1191,7 @@ def group_search(request, extra_context=None, **kwargs):
 
 
 @require_http_methods(["GET", "POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_all(request, extra_context=None, **kwargs):
     q = AstakosGroup.objects.select_related()
     q = q.filter(~Q(kind__name='default'))
@@ -1216,8 +1246,7 @@ def group_all(request, extra_context=None, **kwargs):
 
 #@require_http_methods(["POST"])
 @require_http_methods(["POST", "GET"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_join(request, group_id):
     m = Membership(group_id=group_id,
                    person=request.user,
@@ -1236,8 +1265,7 @@ def group_join(request, group_id):
 
 
 @require_http_methods(["POST"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def group_leave(request, group_id):
     try:
         m = Membership.objects.select_related().get(
@@ -1276,8 +1304,7 @@ def handle_membership(func):
 
 #@require_http_methods(["POST"])
 @require_http_methods(["POST", "GET"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 @handle_membership
 def approve_member(request, membership):
     try:
@@ -1295,8 +1322,7 @@ def approve_member(request, membership):
         messages.error(request, msg)
 
 
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 @handle_membership
 def disapprove_member(request, membership):
     try:
@@ -1312,8 +1338,7 @@ def disapprove_member(request, membership):
 
 #@require_http_methods(["GET"])
 @require_http_methods(["POST", "GET"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def resource_usage(request):
     def with_class(entry):
         entry['load_class'] = 'red'
@@ -1426,8 +1451,7 @@ def group_create_list(request):
 
 #@require_http_methods(["GET"])
 @require_http_methods(["POST", "GET"])
-@signed_terms_required
-@login_required
+@valid_astakos_user_required
 def timeline(request):
 #    data = {'entity':request.user.email}
     timeline_body = ()