From b71cde7d79fbadc83aecb3cc83c37fa190df2c1e Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Mon, 13 Feb 2023 14:49:19 +0100
Subject: [PATCH] user-detail: display planned alert & deletion dates (#74464)

---
 .../authentic2/manager/user_detail.html       | 17 +++++++
 src/authentic2/manager/user_views.py          | 34 ++++++++++++++
 tests/test_user_manager.py                    | 46 ++++++++++++++++++-
 3 files changed, 96 insertions(+), 1 deletion(-)

diff --git a/src/authentic2/manager/templates/authentic2/manager/user_detail.html b/src/authentic2/manager/templates/authentic2/manager/user_detail.html
index b258c8fe..2477498b 100644
--- a/src/authentic2/manager/templates/authentic2/manager/user_detail.html
+++ b/src/authentic2/manager/templates/authentic2/manager/user_detail.html
@@ -85,6 +85,23 @@
       </p>
     {% endif %}
 
+    {% if alert_date and deletion_date %}
+      <p class="a2-manager-user-date-alert">
+        {% if alert_date > now %}
+          {% blocktrans %}Deletion alert email planned for {{ alert_date }}.{% endblocktrans %}
+        {% else %}
+          {% blocktrans %}Deletion alert email sent on {{ alert_date }}.{% endblocktrans %}
+        {% endif %}
+      </p>
+      <p class="a2-manager-user-date-deletion">
+        {% if deletion_date > now %}
+          {% blocktrans %}Account deletion planned for {{ deletion_date }}.{% endblocktrans %}
+        {% else %}
+          {% blocktrans %}Account deletion performed on {{ deletion_date }}.{% endblocktrans %}
+        {% endif %}
+      </p>
+    {% endif %}
+
     {% for data in user_data %}
       {{ data }}
     {% endfor %}
diff --git a/src/authentic2/manager/user_views.py b/src/authentic2/manager/user_views.py
index e37689a5..89b1806c 100644
--- a/src/authentic2/manager/user_views.py
+++ b/src/authentic2/manager/user_views.py
@@ -16,6 +16,7 @@
 
 import base64
 import collections
+import datetime
 import operator
 
 from django.contrib import messages
@@ -30,6 +31,7 @@ from django.urls import reverse, reverse_lazy
 from django.utils import timezone
 from django.utils.functional import cached_property
 from django.utils.html import format_html
+from django.utils.timezone import now
 from django.utils.translation import gettext
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import pgettext_lazy
@@ -40,6 +42,7 @@ from django.views.generic.edit import BaseFormView
 from authentic2.a2_rbac.models import OrganizationalUnit, Role, RoleParenting
 from authentic2.a2_rbac.utils import get_default_ou
 from authentic2.apps.journal.views import JournalViewWithContext
+from authentic2.backends.ldap_backend import LDAPBackend
 from authentic2.models import Attribute, PasswordReset
 from authentic2.utils import hooks, spooler, switch_user
 from authentic2.utils.misc import is_ajax, make_url, redirect, select_next_url, send_password_reset_mail
@@ -437,6 +440,37 @@ class UserDetailView(OtherActionsMixin, BaseDetailView):
             data for datas in hooks.call_hooks('manager_user_data', self, self.object) for data in datas
         ]
         kwargs['user_data'] = user_data
+
+        realms = [block['realm'] for block in LDAPBackend.get_config() if block.get('realm')]
+        # user is active and belongs to an OU that defines deletion delays
+        if (
+            self.object.is_active
+            and self.object.email
+            and self.object.ou
+            and self.object.ou.clean_unused_accounts_alert
+            and self.object.ou.clean_unused_accounts_deletion
+        ):
+            # user does not have any external identifier that would prohibit automated deletion
+            if not (
+                getattr(self.object, 'oidc_account', None)
+                or (
+                    getattr(self.object, 'userexternalid', None)
+                    and getattr(self.object.userexternalid, 'source', None)
+                    and self.object.userexternalid.source in realms
+                )
+            ):
+                # base value for computing alert & deletion is creation date or last login
+                start = self.object.date_joined or self.object.last_login
+                # but the keepalive alert date is more relevant if more recent
+                if self.object.keepalive and self.object.keepalive > start:
+                    start = self.object.keepalive
+                kwargs['now'] = now().date()
+                kwargs['alert_date'] = start.date() + datetime.timedelta(
+                    days=self.object.ou.clean_unused_accounts_alert
+                )
+                kwargs['deletion_date'] = start.date() + datetime.timedelta(
+                    days=self.object.ou.clean_unused_accounts_deletion
+                )
         ctx = super().get_context_data(**kwargs)
         return ctx
 
diff --git a/tests/test_user_manager.py b/tests/test_user_manager.py
index 61a82108..8047908a 100644
--- a/tests/test_user_manager.py
+++ b/tests/test_user_manager.py
@@ -756,7 +756,7 @@ def test_user_import_attributes(transactional_db, app, admin):
     assert et.attributes.values['phone'].content == '+3281123456'
 
 
-def test_detail_view(app, admin, simple_user):
+def test_detail_view(app, admin, simple_user, freezer, user_ou1, ou1):
     url = f'/manage/users/{simple_user.pk}/'
     resp = login(app, admin, url)
     assert str(simple_user.uuid) in resp.text
@@ -766,6 +766,50 @@ def test_detail_view(app, admin, simple_user):
     simple_user.save()
     resp = app.get(url)
     assert "Last activity on Feb. 1, 2023" in resp.pyquery('.a2-manager-user-last-activity')[0].text
+    logout(app)
+
+    ou1.clean_unused_accounts_alert = 700
+    ou1.clean_unused_accounts_deletion = 730
+    ou1.save()
+    user_ou1.date_joined = user_ou1.last_login = datetime.datetime(2023, 1, 1, 3)
+    user_ou1.save()
+
+    url = f'/manage/users/{user_ou1.pk}/'
+
+    freezer.move_to('2023-01-01')
+    resp = login(app, admin, url)
+    assert (
+        "Deletion alert email planned for Dec. 1, 2024."
+        in resp.pyquery('.a2-manager-user-date-alert')[0].text
+    )
+    assert (
+        "Account deletion planned for Dec. 31, 2024."
+        in resp.pyquery('.a2-manager-user-date-deletion')[0].text
+    )
+    logout(app)
+
+    freezer.move_to('2024-12-10')
+    resp = login(app, admin, url)
+    assert "Deletion alert email sent on Dec. 1, 2024." in resp.pyquery('.a2-manager-user-date-alert')[0].text
+    assert (
+        "Account deletion planned for Dec. 31, 2024."
+        in resp.pyquery('.a2-manager-user-date-deletion')[0].text
+    )
+    logout(app)
+
+    freezer.move_to('2025-01-01')
+    resp = login(app, admin, url)
+    assert "Deletion alert email sent on Dec. 1, 2024." in resp.pyquery('.a2-manager-user-date-alert')[0].text
+    assert (
+        "Account deletion performed on Dec. 31, 2024."
+        in resp.pyquery('.a2-manager-user-date-deletion')[0].text
+    )
+
+    ou1.clean_unused_accounts_alert = ou1.clean_unused_accounts_deletion = None
+    ou1.save()
+    resp = app.get(url)
+    assert not resp.pyquery('.a2-manager-user-date-alert')
+    assert not resp.pyquery('.a2-manager-user-date-deletion')
 
 
 def test_detail_view_user_deleted(app, admin, simple_user):
-- 
2.39.1

