From 504dc4226d3ddd5ed8a7161fa579ea43f54be315 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Fri, 21 Jan 2022 11:46:46 +0100
Subject: [PATCH 2/2] profile: add csam account-(un)linking capabilities
 (#60837)

---
 src/authentic2_auth_fedict/authenticators.py  |  2 -
 src/authentic2_auth_fedict/backends.py        | 48 +++++++++++++++++++
 .../authentic2_auth_fedict/profile.html       | 27 +++++++++--
 src/authentic2_auth_fedict/urls.py            |  1 +
 src/authentic2_auth_fedict/views.py           | 16 ++++++-
 5 files changed, 88 insertions(+), 6 deletions(-)

diff --git a/src/authentic2_auth_fedict/authenticators.py b/src/authentic2_auth_fedict/authenticators.py
index bd83e33..a18425d 100644
--- a/src/authentic2_auth_fedict/authenticators.py
+++ b/src/authentic2_auth_fedict/authenticators.py
@@ -49,8 +49,6 @@ class FedictAuthenticator(BaseAuthenticator):
     def profile(self, request, *args, **kwargs):
         context = kwargs.get('context', {}).copy()
         user_saml_identifiers = request.user.saml_identifiers.all()
-        if not user_saml_identifiers:
-            return ''
         for user_saml_identifier in user_saml_identifiers:
             user_saml_identifier.idp = get_idp(user_saml_identifier.issuer)
         context['user_saml_identifiers'] = user_saml_identifiers
diff --git a/src/authentic2_auth_fedict/backends.py b/src/authentic2_auth_fedict/backends.py
index 4977d30..c975a49 100644
--- a/src/authentic2_auth_fedict/backends.py
+++ b/src/authentic2_auth_fedict/backends.py
@@ -14,8 +14,15 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import logging
+
 import lasso
+from django.contrib import messages
+from django.utils.translation import ugettext_lazy as _
 from mellon.backends import SAMLBackend
+from mellon.utils import get_adapters, get_idp
+
+logger = logging.getLogger(__name__)
 
 
 class FedictBackend(SAMLBackend):
@@ -28,3 +35,44 @@ class FedictBackend(SAMLBackend):
         # but we do not expose this detail to the service provider as all it
         # needs to know is "strong authentication".
         return lasso.SAML2_AUTHN_CONTEXT_SMARTCARD_PKI
+
+    def authenticate(self, request=None, **credentials):
+        saml_attributes = credentials.get('saml_attributes') or {}
+        if 'issuer' not in saml_attributes:
+            logger.debug('no idp in saml_attributes')
+            return None
+        idp = get_idp(saml_attributes['issuer'])
+        if not idp:
+            logger.debug('unknown idp %s', saml_attributes['issuer'])
+            return None
+        adapters = get_adapters(idp, request=request)
+        for adapter in adapters:
+            if not hasattr(adapter, 'authorize'):
+                continue
+            if not adapter.authorize(idp, saml_attributes):
+                return
+        for adapter in adapters:
+            if hasattr(adapter, 'lookup_by_attributes'):
+                user = adapter.lookup_by_attributes(idp, saml_attributes)
+        for adapter in adapters:
+            if not hasattr(adapter, 'lookup_user'):
+                continue
+            user = adapter.lookup_user(idp, saml_attributes)
+            if user:
+                request_user = getattr(request, 'user', None) if request else None
+                if request_user != user and request_user.is_authenticated:
+                    messages.warning(request, _('Your account is now linked to your eID card.'))
+                    saml_identifier = user.saml_identifier
+                    saml_identifier.user = request_user
+                    saml_identifier.save(update_fields=['user'])
+                    user.mark_as_inactive(reason='fedict link to manually-created account')
+                    user = request_user
+                    user.saml_identifier = saml_identifier
+                break
+        else:  # no user found
+            return
+        for adapter in adapters:
+            if not hasattr(adapter, 'provision'):
+                continue
+            adapter.provision(user, idp, saml_attributes)
+        return user
diff --git a/src/authentic2_auth_fedict/templates/authentic2_auth_fedict/profile.html b/src/authentic2_auth_fedict/templates/authentic2_auth_fedict/profile.html
index 7383ab7..35db1f2 100644
--- a/src/authentic2_auth_fedict/templates/authentic2_auth_fedict/profile.html
+++ b/src/authentic2_auth_fedict/templates/authentic2_auth_fedict/profile.html
@@ -1,3 +1,24 @@
-<p>
-Ce compte est relié à votre carte d'identité électronique.
-</p>
+{% load i18n static %}
+<div>
+ <img src="{% static "authentic2_auth_fedict/img/beid_image_mini.png" %}" alt="">
+{% if user_saml_identifiers %}
+ <p>{% blocktrans %}This account is linked to your eID card.{% endblocktrans %}</p>
+ <p>
+ {% blocktrans %}
+ You may want to unlink your account, although you will not be able to
+ connect to your account using your eID card (without re-doing the linking
+ procedure).
+ {% endblocktrans %}
+ </p>
+ <p><a class="button" href="{% url "fedict-unlink" %}">{% trans "Unlink my account" %}</a></p>
+{% else %}
+ <p>
+ {% blocktrans %}
+ You may want to link your existing Publik account to your eID. In order to
+ do so, you will either need your eID card and its card reader
+ or your list of personal tokens.
+ {% endblocktrans %}
+ </p>
+ <p><a class="button" href="{% url "fedict-login" %}?next=/accounts/">{% trans "Link my account to my eID card" %}</a></p>
+{% endif %}
+</div>
diff --git a/src/authentic2_auth_fedict/urls.py b/src/authentic2_auth_fedict/urls.py
index ddb6451..8f74560 100644
--- a/src/authentic2_auth_fedict/urls.py
+++ b/src/authentic2_auth_fedict/urls.py
@@ -21,4 +21,5 @@ from . import views
 urlpatterns = [
     url(r'^accounts/saml/', include('mellon.urls')),
     url(r'^accounts/fedict/login/$', views.login, name='fedict-login'),
+    url(r'^accounts/fedict/unlink/$', views.unlink, name='fedict-unlink'),
 ]
diff --git a/src/authentic2_auth_fedict/views.py b/src/authentic2_auth_fedict/views.py
index ad26f10..5377516 100644
--- a/src/authentic2_auth_fedict/views.py
+++ b/src/authentic2_auth_fedict/views.py
@@ -18,11 +18,13 @@ import random
 import urllib.parse
 
 from django.conf import settings
+from django.contrib import messages
 from django.core import signing
 from django.db import transaction
 from django.http import HttpResponseRedirect
-from django.shortcuts import resolve_url
+from django.shortcuts import redirect, resolve_url
 from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import View
 
@@ -69,3 +71,15 @@ class LoginView(mellon.views.LoginView):
 
 
 login = transaction.non_atomic_requests(csrf_exempt(LoginView.as_view()))
+
+
+def unlink(request):
+    if not hasattr(request, 'user') or not hasattr(request.user, 'saml_identifiers'):
+        return
+    unlink_performed = False
+    for saml_identifier in request.user.saml_identifiers.all():
+        saml_identifier.delete()
+        unlink_performed = True
+    if unlink_performed:
+        messages.success(request, message=_('Unlinking complete.'))
+    return redirect('account_management')
-- 
2.34.1

