Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Migrer des users d'un framework X vers Django ?

+5 votes

Quand on récupère une base de données issue d'un site quelconque, on va probablement récupérer des utilisateurs, avec un champ mot de passe. Sauf que ce champ ne contient évidemment pas de mot de passe, mais un hash, qui a été généré à partir du mot de passe. Et comme vous le savez, on ne peut pas retrouver le mot de passe depuis le hash, c'est tout le but.

Maintenant, si je veux migrer ce site web vers Django, comment est-ce que je peux faire pour récupérer et reconstruire les utilisateurs ? Puisque Django a sa propre manière de calculer un hash, il me semble évident que copier coller de manière brute le mot de passe de la base X vers la base de l'app Django ne va pas fonctionner.

Y a t-il une solution à ce problème ? Ca doit être totalement transparent pour l'utilisateur.

demandé 21-Mai-2015 par walt (230 points)

2 Réponses

+6 votes
 
Meilleure réponse

J'ai été confronté à ce problème il y a quelque temps (migration d'un site PHP vers django) et nous ne voulions pas obliger l'ensemble des utilisateurs à changer leur mot de passe (les passwords étaient salés avec une clé fixe en SHA puis hashés).

Pour résoudre cette situation, nous avons simplement créé notre propre AUTHENTICATION_BACKEND, qui se charge d'authentifier les utilisateurs dont le mot de passe n'a pas été changé depuis la migration.

Sur le profil utilisateur, on rajoute également un champ password_changed_date et un champ legacy_id qui contient l'ID initial de l'utilisateur. Cela permet de n'utiliser le backend que lorsque nécessaire.

Concrètement, le code ressemble à ce qui suit.

Pour la partie profil:

from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save

from django.utils import timezone


class UserProfile(models.Model):

    user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile')
    legacy_id = models.IntegerField(null=True, blank=True, unique=True)
    legacy_password_change_date = models.DateTimeField(null=True, blank=True)

def update_change_password_date(sender, instance, **kwargs):
    # if model is just created, we do nothing
    try:
        db_user = get_user_model().objects.get(username=instance.username)
    except get_user_model().DoesNotExist:
        return

    # password has changed, we update the change date
    if db_user.password != instance.password:
        instance.profile.password_change_date = timezone.now()
        instance.profile.save()

pre_save.connect(update_change_password_date, sender=settings.AUTH_USER_MODEL)

Pour le backend:

from __future__ import unicode_literals
import hashlib

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend


def check_hash(user, password):
    """Notre fonction legacy de vérification de mot de passe

    hashed_password = something # à toi d'implémenter ton hashage legacy
    return user.password == hashed_password


class LegacyModelBackend(ModelBackend):
    """l'essentiel du code est copié depuis le ModelBackend de django"""

    def authenticate(self, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
            # on vérifie que l'utilisateur est bien legacy est n'a jamais changé son mot de passe
            if user.profile.legacy_id and user.profile.password_changed_date is None:
                if check_hash(user, password):
                    return user

        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a non-existing user (#20760).
            UserModel().set_password(password)

Dans les settings, ils te reste à rajouter ton backend:

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'legacy.backends.LegacyModelBackend',
)

Chez nous, ça marche bien, même couplé à d'autres solutions (comme django-allauth, pour l'authentification OAuth).

Il me semble que c'est une solution plutôt propre pour gérer la situation, même si tu peux choisir d'implémenter les choses différement.

Par exemple, si tu utilise déjà un modèle utilisateur personnalisé, pas besoin de t'embéter à créer un profil. Tu peux mettre les champs directement sur le modèle utilisateur.

Bien entendu, ça reste toujours moins propre que de réinitialiser tous les mot de passe et de partir sur une base clean, puisque cela alourdit le code. Néanmoins, repartir à zéro n'est pas toujours envisageable.

répondu 21-Mai-2015 par eliotberriot (678 points)
sélectionné 21-Mai-2015 par walt

Un grand merci parce que c'est exactement le genre de réponse qu'il me fallait, c'est super clair, et me fait croire que c'est pas impossible de passer de Symphony à Django même une fois en prod avec des utilisateurs !

Ça doit être sympa comme projet de quitter ce framework la pour Django !

Walt, content d'avoir pu t'aider. Pour la partie base de données / migration, je te recommande vivement la commande inspectdb qui permet de générer des modèles django à partir d'une base existante.

Couplé a un router, ça nous a permis d'utiliser l'ORM django pour requêter la base legacy dans nos scripts de migration ce qui, comme tu t'en doute, fait gagner un temps considérable par rapport à du SQL normal.

N'hésite pas si tu as d'autres questions dans le genre, faut se serrer les coudes entre migrants PHP -> Python ;)

Il semble qu'une autre voie possible (et peut-être plus clean) soit de créer ta propre fonction de hash de password et de l'enregistrer dans les settings. C'est sûrement mieux en fait, il faudra que je teste.

Ah très bien pour inspectdb je connaissais pas, merci !
A vrai dire c'est pas tant la définition des models qui me fait peur, mais c'est d'importer les données de ma base legacy dans la nouvelle (qui ne sera peut être pas 100% identique).
J'aimerais scripter ça évidemment, il faudra que je vois comment faire ça proprement, là j'ai pas encore les idées claires sur le "comment" pour cette partie.
Merci encore pour tes conseils.

On a exploré une piste a base de commandes django pour l'import des données. Si tu ouvre une nouvelle question, je pourrais te poster un peu de code quand j'aurais un moment.

+3 votes

Plusieurs aspects dans cette "migration" à mon sens :

A - La transparence :

AMHA il faut être transparent avec eux sinon, et prévenir chaque utilisateur par mail de ce qui va se passer du genre "suite à un changement de système vous devrez suivre la procédure suivante ..."
car s'ils découvrent le pot aux roses ca peut etre pire que tout faire en cachette et très fortement se planter / faire fuir les utlisateurs qui perdront confiance dans le site/l'application

Ensuite 2 cas de figure:

le site gère les utilisateurs à partir d'une inscription de leur part donc là, il faut :

  1. prévenir chaque utilisateur par mail de ce qui va se passer du genre "suite à un changement de système vous devrez suivre la procédure 'j ai oublié mon mot de passe' pour le réinitialiser votre mot de passe.
  2. il faut rendre le compte de tous les utilisateurs dans l'état "en attente de validation du compte" (ceci dépendra du systeme de gestion d'inscription choisi, puisqu'il n'en existe pas par defaut dans django)

si le site ne gère pas d'inscription mais qu'ils sont gérés par un admin par exemple, il faut faire un script qui produit tout seul comme un grand :

  1. un mot de passe aléatoire
  2. le stock dans la base dans la version "hashée"
  3. envoi un mail avec ce mot de passe indiquant à l'utilisateur que c'est un mail automatique ne stockant pas du tout le mot de passe en clair et qu'il ne le perde pas, dans ce mail on ne fourni pas le login de l'utilisateur dans le mail. S'il vous voulez quand meme fournir le login de l'utilisateur, faites 2 mails séparés l'un rappelant le login le second fournissant le nouveau mot de passe.

B - la migration de la table des utilisateurs du framework X
pour cette partie j'utiliserai ce que Django permet : un AUTH_USER_MODEL
Ceci permet de créer un lien OneToOne entre le model User de Django et celui de la base existante
Je pense que ça devrait suffir.
Alors bien evidement, le modèle User de django possède un login et un mot de passe, donc le script "qui fait tout tout seul", évoqué ci dessus, devrait aller écrire les données dans le model User de django.

Voila la piste que je suivrai ;)
Bon courage

répondu 21-Mai-2015 par foxmask (2,888 points)

Merci pour ta réponse Foxmask.
Mais pour moi, demander aux utilisateurs de changer leur mot de passe n'est pas acceptable comme solution, on parle pas d'un petit projet de hobbie mais d'un site commercial avec beaucoup d'utilisateurs.

Je pensais plutôt à une idée pour modifier la vérification de mot de passe de Django pour qu'elle soit capable de prendre un compte des hash qui sont un peu différents de ce que le framework créérait par défaut.

D'ailleurs entre temps, j'ai trouvé un exemple avec Drupal sur ce post qui explique comment faire.

J'étais parti du principe qu'on ignorait comment les mots de passe avait été hashe ,désolé .

...