From c0590aea7981916db15be025522c946a78eb8a65 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Sat, 1 Aug 2020 09:18:46 +0200
Subject: [PATCH] profile_views: include ous in oidc-authz management page
 (#45651)

---
 .../templates/authentic2/accounts.html        |  2 +-
 .../accounts_authorized_oauth_services.html   | 52 +++++++++++++++++++
 src/authentic2/views.py                       | 29 ++++++++---
 tests/test_idp_oidc.py                        | 34 ++++++++++--
 4 files changed, 103 insertions(+), 14 deletions(-)

diff --git a/src/authentic2/templates/authentic2/accounts.html b/src/authentic2/templates/authentic2/accounts.html
index d5cc82f69..83ff8b97a 100644
--- a/src/authentic2/templates/authentic2/accounts.html
+++ b/src/authentic2/templates/authentic2/accounts.html
@@ -38,7 +38,7 @@
         <p><a href="{% url 'profile_edit' %}">{% trans "Edit account data" %}</a></p>
       {% endif %}
       {% if allow_authorization_management %}
-        <p><a href="{% url 'authorized-oauth-services' %}">{% trans "Manage service authorizations" %}</a></p>
+        <p><a href="{% url 'authorized-oauth-services' %}">{% trans "Manage OUs and services authorizations" %}</a></p>
       {% endif %}
       {% if allow_account_deletion %}
         <p><a href="{% url 'delete_account' %}">{% trans "Delete account" %}</a></p>
diff --git a/src/authentic2/templates/authentic2/accounts_authorized_oauth_services.html b/src/authentic2/templates/authentic2/accounts_authorized_oauth_services.html
index be72fa426..67d5851e9 100644
--- a/src/authentic2/templates/authentic2/accounts_authorized_oauth_services.html
+++ b/src/authentic2/templates/authentic2/accounts_authorized_oauth_services.html
@@ -12,6 +12,58 @@
 {% endblock %}
 
 {% block content %}
+{% block oidc-authorized-oauth-ous-pre %}{% endblock %}
+<div class="authorized-oauth-ous">
+  {% block oidc-authorized-oauth-ous-top %}
+  <p class="authorized-oauth-ous--top">
+    {% if authorized_oauth_ous|length_is:0 %}
+    {% trans "You have not granted organizational unit access to your account profile data." %}
+    {% else %}
+    {% blocktrans count counter=authorized_oauth_ous|length %}
+    You have granted one organizational unit access to your account profile data.
+    {% plural %}
+    You have granted {{ counter }} organizational units access to your account profile data.
+    {% endblocktrans %}
+    {% endif %}
+  </p>
+  {% endblock %}
+  <ul class="authorized-oauth-ous--list">
+    {% for auth in authorized_oauth_ous %}
+    <li class="authorized-oauth-ous--item">
+      <form method="post" class="authorized-oauth-ous--form">
+        {% csrf_token %}
+        {% block oidc-authorized-oauth-ou %}
+        <div class="authorized-oauth-ous--infos">
+          {% block oidc-authorized-oauth-ou-top %}{% endblock %}
+          <span class="authorized-oauth-ous--client">
+            {{ auth.client }}
+          </span>
+          <span class="authorized-oauth-ous--dates">
+            <span class="authorized-oauth-ous-dates--since">
+              <span class="label">{% trans "Allowed since:" %}</span>
+              <span class="time">{{ auth.created }}</span>
+            </span>
+            <span class="authorized-oauth-ous--separator">/</span>
+            <span class="authorized-oauth-ous--expired">
+              <span class="label">{% trans "Expire on:" %}</span>
+              <span class="time">{{ auth.expired }}</span>
+            </span>
+          </span>
+        </div>
+        <div class="authorized-oauth-ous--actions">
+          <input type="hidden" id="auth-id" name="auth_id" value="{{ auth.id }}">
+          <button class="authorized-oauth-ous--revoke-button">{% trans 'Revoke' %}</button>
+        </div>
+        {% block oidc-authorized-oauth-ou-bottom %}{% endblock %}
+        {% endblock %}
+      </form>
+    </li>
+    {% endfor %}
+  </ul>
+</table>
+{% block oidc-authorized-oauth-ous-bottom %}{% endblock %}
+</div>
+{% block oidc-authorized-oauth-ous-post %}{% endblock %}
 {% block oidc-authorized-oauth-services-pre %}{% endblock %}
 <div class="authorized-oauth-services">
   {% block oidc-authorized-oauth-services-top %}
diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index aa6b0ad0f..ee6a10a7a 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -23,6 +23,7 @@ import re
 from ratelimit.utils import is_ratelimited
 
 from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
 from django.shortcuts import render, get_object_or_404
 from django.template.loader import render_to_string
 from django.views.generic.edit import UpdateView, FormView
@@ -512,12 +513,13 @@ class ProfileView(cbv.TemplateNamesMixin, TemplateView):
             'federation_management': federation_management,
         })
 
-        if ('authentic2_idp_oidc' in settings.INSTALLED_APPS and
-                app_settings.A2_PROFILE_CAN_MANAGE_SERVICE_AUTHORIZATIONS):
-            from authentic2_idp_oidc.models import OIDCClient
-            context['allow_authorization_management'] = OIDCClient.objects.filter(
-                    authorization_mode=OIDCClient.AUTHORIZATION_MODE_BY_SERVICE).exists()
-
+        if 'authentic2_idp_oidc' in settings.INSTALLED_APPS:
+            if app_settings.A2_PROFILE_CAN_MANAGE_SERVICE_AUTHORIZATIONS:
+                from authentic2_idp_oidc.models import OIDCClient
+                context['allow_authorization_management'] = OIDCClient.objects.filter(
+                        authorization_mode__in=(
+                            OIDCClient.AUTHORIZATION_MODE_BY_SERVICE,
+                            OIDCClient.AUTHORIZATION_MODE_BY_OU)).exists()
         hooks.call_hooks('modify_context_data', self, context)
         return context
 
@@ -1285,16 +1287,27 @@ class AuthorizedOauthServicesView(TemplateView):
 
     def get_context_data(self, **kwargs):
         from authentic2_idp_oidc.models import OIDCAuthorization
+        from authentic2_idp_oidc.models import OIDCClient
+        from django_rbac.utils import get_ou_model
 
         context = super(AuthorizedOauthServicesView, self).get_context_data(**kwargs)
+        service_ct = ContentType.objects.get_for_model(OIDCClient)
+        ou_ct = ContentType.objects.get_for_model(get_ou_model())
         context['authorized_oauth_services'] = OIDCAuthorization.objects.filter(
-            user=self.request.user)
+            user=self.request.user, client_ct=service_ct)
+        context['authorized_oauth_ous'] = OIDCAuthorization.objects.filter(
+            user=self.request.user, client_ct=ou_ct)
         return context
 
     def post(self, request, *args, **kwargs):
         from authentic2_idp_oidc.models import OIDCAuthorization
+        from authentic2_idp_oidc.models import OIDCClient
+        from django_rbac.utils import get_ou_model
 
-        qs = OIDCAuthorization.objects.filter(user=request.user)
+        service_ct = ContentType.objects.get_for_model(OIDCClient)
+        ou_ct = ContentType.objects.get_for_model(get_ou_model())
+        qs = OIDCAuthorization.objects.filter(
+            user=request.user, client_ct__in=(service_ct, ou_ct))
         auth_id = request.POST.get('auth_id')
         if auth_id:
             qs = qs.filter(id=auth_id)
diff --git a/tests/test_idp_oidc.py b/tests/test_idp_oidc.py
index 261c1b7a9..53c73031d 100644
--- a/tests/test_idp_oidc.py
+++ b/tests/test_idp_oidc.py
@@ -45,6 +45,7 @@ from authentic2_idp_oidc.utils import make_sub
 from authentic2.a2_rbac.utils import get_default_ou
 from authentic2.utils import make_url
 from authentic2_auth_oidc.utils import parse_timestamp
+from django_rbac.utils import get_ou_model
 from django_rbac.utils import get_role_model
 
 User = get_user_model()
@@ -1615,6 +1616,8 @@ def test_oidc_client_clean():
 
 
 def test_oidc_authorized_oauth_services_view(app, oidc_client, simple_user):
+    from django.contrib.contenttypes.models import ContentType
+
     url = make_url('authorized-oauth-services')
     response = app.get(url, status=302)
     assert '/login/' in response.location
@@ -1632,19 +1635,40 @@ def test_oidc_authorized_oauth_services_view(app, oidc_client, simple_user):
     OIDCAuthorization.objects.create(
         client=oidc_client, user=simple_user, scopes='openid profile email',
         expired=now() + datetime.timedelta(days=2))
+    # create an ou-based authz that appears at the top of the page
+    OU = get_ou_model()
+    ou1 = OU.objects.create(name='Orgunit1', slug='orgunit1')
+    OIDCAuthorization.objects.create(
+        client=ou1, user=simple_user, scopes='openid profile email',
+        expired=now() + datetime.timedelta(days=2))
 
     response = app.get(url, status=200)
-    assert "You have granted 3 services access to your account profile data."
+    assert "You have granted one organizational unit access to your account profile data." in response.text
+    assert "You have granted 3 services access to your account profile data." in response.text
+    assert len(response.html.find_all(
+        'button', {'class': 'authorized-oauth-ous--revoke-button'})) == 1
     assert len(response.html.find_all(
         'button', {'class': 'authorized-oauth-services--revoke-button'})) == 3
 
-    # revoke two
-    response = response.forms[0].submit()
+    # revoke two service authz
+    response = response.forms[1].submit()
     response = response.follow()
     assert len(response.html.find_all(
         'button', {'class': 'authorized-oauth-services--revoke-button'})) == 2
-    response = response.forms[0].submit()
+    assert OIDCAuthorization.objects.filter(
+        client_ct=ContentType.objects.get_for_model(OIDCClient)).count() == 2
+    response = response.forms[1].submit()
     response = response.follow()
+    assert "You have granted one service access to your account profile data." in response.text
     assert len(response.html.find_all(
         'button', {'class': 'authorized-oauth-services--revoke-button'})) == 1
-    assert "You have granted one service access to your account profile data." in response.text
+    assert OIDCAuthorization.objects.filter(
+        client_ct=ContentType.objects.get_for_model(OIDCClient)).count() == 1
+
+    # revoke the only OU authz
+    response = response.forms[0].submit()
+    response = response.follow()
+    assert len(response.html.find_all(
+        'button', {'class': 'authorized-oauth-ous--revoke-button'})) == 0
+    assert OIDCAuthorization.objects.filter(
+        client_ct=ContentType.objects.get_for_model(OU)).count() == 0
-- 
2.28.0

