From 761bf8015c676448914b8eb9c2da2dd27e0b87ee Mon Sep 17 00:00:00 2001
From: Dan Braghis <dan.braghis@torchbox.com>
Date: Thu, 11 Jan 2018 15:28:12 +0000
Subject: [PATCH] Add separate view for OAuth associations

and allow disconnecting
---
 opentech/apply/users/decorators.py            | 20 +++++++++
 .../apply/users/templates/users/account.html  |  9 +++-
 .../apply/users/templates/users/oauth.html    | 41 +++++++++++++++++++
 opentech/apply/users/templatetags/__init__.py |  0
 .../apply/users/templatetags/users_tags.py    | 40 ++++++++++++++++++
 opentech/apply/users/urls.py                  |  3 +-
 opentech/apply/users/views.py                 | 13 ++++++
 7 files changed, 123 insertions(+), 3 deletions(-)
 create mode 100644 opentech/apply/users/decorators.py
 create mode 100644 opentech/apply/users/templates/users/oauth.html
 create mode 100644 opentech/apply/users/templatetags/__init__.py
 create mode 100644 opentech/apply/users/templatetags/users_tags.py

diff --git a/opentech/apply/users/decorators.py b/opentech/apply/users/decorators.py
new file mode 100644
index 000000000..11539ef29
--- /dev/null
+++ b/opentech/apply/users/decorators.py
@@ -0,0 +1,20 @@
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+
+
+def require_oauth_whitelist(view_func):
+    """Simple decorator that limits the use of OAuth to the configure whitelisted domains"""
+    def decorated_view(request, *args, **kwargs):
+        user = request.user
+
+        try:
+            if settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS:
+                for domain in settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS:
+                    if user.email.endswith(f'@{domain}'):
+                        return view_func(request, *args, **kwargs)
+        except AttributeError:
+            raise PermissionDenied
+
+        raise PermissionDenied
+
+    return decorated_view
diff --git a/opentech/apply/users/templates/users/account.html b/opentech/apply/users/templates/users/account.html
index 725d61531..5873285ed 100644
--- a/opentech/apply/users/templates/users/account.html
+++ b/opentech/apply/users/templates/users/account.html
@@ -1,12 +1,17 @@
 {% extends 'base.html' %}
-{% load i18n %}
+{% load i18n users_tags %}
 
 {% block title %}Account{% endblock %}
 
 {% block content %}
 <h2>Welcome {{ user }}</h2>
 
-{% if show_change_password %}
+{% if show_change_password and not backends.associated %}
     <a href="{% url 'users:password_change' %}">{% trans "Change password" %}</a>
 {% endif %}
+
+{% can_use_oauth as show_oauth_link %}
+{% if show_oauth_link %}
+    <a href="{% url 'users:oauth' %}">{% trans "Manage OAuth" %}</a>
+{% endif %}
 {% endblock %}
diff --git a/opentech/apply/users/templates/users/oauth.html b/opentech/apply/users/templates/users/oauth.html
new file mode 100644
index 000000000..312003ddd
--- /dev/null
+++ b/opentech/apply/users/templates/users/oauth.html
@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+{% load users_tags %}
+
+{% block title %}OAuth{% endblock %}
+
+{% block content %}
+<div class="social">
+    {% if backends.associated %}
+        <h3>Providers</h3>
+        <ul>
+        {% for association in backends.associated %}
+            <li>
+                {% if user.password %}
+                    <form id="{{ association.provider|backend_name }}-disconnect" class="disconnect-form col-md-2" action="{% url 'social:disconnect_individual' backend=association.provider association_id=association.id %}?next={% url "users:account" %}"
+                        method="post">
+                        <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
+                        <button class="btn btn-danger" name="{{ association.provider|backend_class }}">
+                            Disconnect {{ association.provider|backend_name }}
+                        </button>
+                    </form>
+                {% else %}
+                    {{ association.provider|backend_name }}
+                {% endif %}
+            </li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+    {% if backends.not_associated %}
+        <h3>Available OAuth providers</h3>
+        <ul>
+        {% for backend in backends.not_associated %}
+            <li>
+                <a id="{{ name }}-button" class="col-md-2 btn btn-default" name="{{ backend }}" href="{% url 'social:begin' backend=backend %}">
+                    Associate with {{ backend|backend_name }}
+                </a>
+            </li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+</div>
+{% endblock %}
diff --git a/opentech/apply/users/templatetags/__init__.py b/opentech/apply/users/templatetags/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/opentech/apply/users/templatetags/users_tags.py b/opentech/apply/users/templatetags/users_tags.py
new file mode 100644
index 000000000..0125e89bd
--- /dev/null
+++ b/opentech/apply/users/templatetags/users_tags.py
@@ -0,0 +1,40 @@
+from django import template
+from django.conf import settings
+
+register = template.Library()
+
+
+@register.filter
+def backend_name(name):
+    return {
+        'google-oauth': 'Google OAuth',
+        'google-oauth2': 'Google OAuth',
+        'google-openidconnect': 'Google OpenId',
+        'facebook-app': 'Facebook',
+        'stackoverflow': 'Stack Overflow',
+        'yahoo-oauth': 'Yahoo',
+        'vimeo': 'Vimeo',
+        'linkedin-oauth2': 'LinkedIn OAuth',
+        'vk-oauth2': 'VK OAuth',
+        'live': 'Windows Live',
+    }.get(name, name)
+
+
+@register.filter
+def backend_class(backend):
+    return backend.replace('-', ' ')
+
+
+@register.simple_tag(takes_context=True)
+def can_use_oauth(context):
+    user = context.get('user')
+
+    try:
+        if settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS:
+            for domain in settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS:
+                if user.email.endswith(f'@{domain}'):
+                    return True
+    except AttributeError:
+        return False
+
+    return False
diff --git a/opentech/apply/users/urls.py b/opentech/apply/users/urls.py
index 4942e2dad..1fc074d79 100644
--- a/opentech/apply/users/urls.py
+++ b/opentech/apply/users/urls.py
@@ -2,7 +2,7 @@ from django.conf.urls import url
 from django.contrib.auth import views as auth_views
 from django.urls import reverse_lazy
 
-from opentech.apply.users.views import account
+from opentech.apply.users.views import account, oauth
 
 urlpatterns = [
     url(r'^$', account, name='account'),
@@ -53,4 +53,5 @@ urlpatterns = [
         auth_views.PasswordResetCompleteView.as_view(template_name='users/password_reset/complete.html'),
         name='password_reset_complete'
     ),
+    url(r'^oauth$', oauth, name='oauth'),
 ]
diff --git a/opentech/apply/users/views.py b/opentech/apply/users/views.py
index c531b183a..c9bad14df 100644
--- a/opentech/apply/users/views.py
+++ b/opentech/apply/users/views.py
@@ -1,9 +1,12 @@
 from django.contrib.auth.decorators import login_required
 from django.shortcuts import render
+from django.template.response import TemplateResponse
 from django.urls import reverse_lazy
 
 from wagtail.wagtailadmin.views.account import password_management_enabled
 
+from .decorators import require_oauth_whitelist
+
 
 @login_required(login_url=reverse_lazy('users:login'))
 def account(request):
@@ -12,3 +15,13 @@ def account(request):
     return render(request, 'users/account.html', {
         'show_change_password': password_management_enabled() and request.user.has_usable_password(),
     })
+
+
+@login_required(login_url=reverse_lazy('users:login'))
+@require_oauth_whitelist
+def oauth(request):
+    """
+    Generic, empty view for the OAuth associations.
+    """
+
+    return TemplateResponse(request, 'users/oauth.html', {})
-- 
GitLab