arrow_back
Conversation résolue thumb_up

Django : POST + paramètre dans l'URL

Re bonjour ^^

Une vue basique : add to cart

path("add-to-cart/<int:pk>/", add_to_cart, name="add-to-cart"),

Le html :

<form action="{% url 'shop:add-to-cart' pk=garment.pk %}" method="post">
                        {% csrf_token %}
                        <input class="btn btn-outline-dark mt-auto" type="submit" value="Echanger"/>
</form>

La vue :

@require_POST
@login_required
def add_to_cart(request, pk):
    user = request.user

    garment = get_object_or_404(klass=Garment, pk=pk)
    cart, _ = Cart.objects.get_or_create(user=user)

    # Vérifier d'abord si dans le panier de l'utilisateur avec message
    if cart.orders.filter(garment__id=garment.id).exists():
        messages.add_message(request, messages.WARNING, f"{garment.description} est déjà dans votre panier")
        return redirect(garment)

    # Vérifier sinon si dans un panier tout court avec message (mais différent que le précédent)
    elif Cart.objects.filter(orders__garment__id=garment.id).exists():
        messages.add_message(request, messages.WARNING, f"{garment.description} est déjà dans un panier")
        return redirect(garment)

    # Sinon ajouter au panier
    else:
        order = Order.objects.create(user=user, garment=garment)
        cart.orders.add(order)
        return redirect("shop:cart")

Donc ici je passe par une méthode post tout en envoyant des données dans l'uRL... Car je modifie l'état de ma BDD !

J'ai vu que c'était une pratique courante de passer des infos dans l'url tout en passant par une POST dès que la vue vers laquelle on est redirigé modifie l'état de la BDD.

Je ne sais pas pourquoi je manifeste cette intérogation que maintenant, car ça fait au moins 7 mois que je fais ça... lol. Car pour moi aucun soucis c'est logique. Mais je préfère en parler ici (mieux vaut tard que jamais), je ne l'ai pas fait il y a 7 mois.

En fait avant (il y a 7 mois) je passais par une get pour ce type d'action, mais on me l'avait fortement déconseillé, et de passer par une post plutot.

Méthode POST obligatoire si modification de la BDDformat_paragraph

Je veux surtout être sur à 100% que si la BDD est modifiée, pas de question à se poser ==> POST d'office sans exception. Ou il peut y avoir des exceptions ?

C'est cette question que je me posais à l'époque. Je n'ai rien loupé je pense ?

Pourquoi je me pose la question que maintenant ? Comme je n'ai pas posé la question à l'époque de temps en temps j'ai un doute et je préfère vérouiler ça... :)

Idempotenteformat_paragraph

C'est aussi surtout pour revenir sur le concepte de l'idempotente.

Pour moi c'est compris, mais est-ce que ça serait possible d'expliquer de manière un peu plus approfindie avec des exemples concrtets de risques réels ? Je pense que ça m'aiderait à mieux me rendre compte.

Merci ! :)

Salut !

Alors, t'as tout à fait raison sur plusieurs points et tu fais bien de vouloir verrouiller cette compréhension.

Utilisation de POST pour les modifications en BDD

Quand il s'agit de transactions qui modifient l'état du serveur (comme ajouter un article à un panier), tu as raison de penser qu'il faut utiliser une méthode HTTP qui reflète cette intention. Le protocole HTTP définit plusieurs méthodes de requête pour indiquer l'action souhaitée :

  • GET est idempotente : appeler 1 fois ou 1000 fois une requête GET sur la même URL aura le même effet (sans changement d'état côté serveur). Je reviens là dessus plus bas ;)
  • POST est utilisé pour soumettre des données à traiter à une ressource spécifiée. Contrairement à GET, soumettre plusieurs fois la même requête POST peut entraîner des résultats différents et/ou des changements d'état côté serveur.

Concept d'idempotence

Une opération est idempotente si l'effet de la réaliser une seule fois est exactement le même que de la réaliser plusieurs fois.

Exemples d'idempotence :

  • GET : Peu importe combien de fois tu demandes une ressource, elle ne changera pas à cause de ces demandes (tu affiches un article de blog, c'est toujours le même article qui t'es retourné).
  • PUT : Si tu envoies une mise à jour de resource, peu importe combien de fois tu l'envois, l'état final sera le même (la dernière valeur mise reste la même).
  • DELETE : Supprimer une ressource sera le même que tu appelles cette commande une ou plusieurs fois; une fois la ressource supprimée, elle reste supprimée.

Si tu utilises GET pour ajouter un article dans un panier (non idempotent), un utilisateur pourrait accidentellement ajouter plusieurs fois le même article en rafraîchissant la page ou en cliquant plusieurs fois sur un lien (parce que le navigateur pourrait mettre en cache la requête et répéter l'action sans interaction supplémentaire de l'utilisateur).

Quand tu soumet un formulaire avec POST, tu as probablement remarqué le petit popup quand tu rafraîchis la page qui te dis que tu vas soumettre de nouveau les données du formulaire. Cela t'indique de la sorte que tu t'appretes à soumettre de nouveau des données au site et que tu peux donc répéter potentiellement une opération.

Avec un GET, tu n'as pas ce popup : tu peux juste accéder à l'URL.

Dans ton cas, tu envoies des données dans l'URL (le pk), mais ce n'est pas ça qui fait que ta requête sera un GET ou un POST, c'est la façon dont tu envoies les données.

Quand tu mets un lien dans ton HTML (avec une balise a), c'est une requête GET. Quand tu soumets un formulaire, ça va dépendre de ce que tu mets dans la paramètre method (par défaut, GET).

Comme indiqué ici 👇 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method

Si tu soumets en avec la méthode GET, les données du formulaire seront envoyées en clair dans l'URL, c'est rarement ce qu'on veut, surtout si tu as des données sensibles comme un mot de passe.

D'accord merci ! C'est plus clair pour idempotente du coup !

C'est bien ce que je pensais.

Donc ici je fais bien les choses ?

Dans tous les cas si quelqu'un appelle la vue en passant les paramètres tel quel dans l'url il y aura une erreur j'ai un require POST.

Dans le code de mon 1er message j'affiche les éléments dans l'url car ils ne sont pas dans mon formulaire mais ce n'est pas sensible, mais j'utilise une POST quand même pour accéder à la vue par un formulaire (comme il y a modification de la bdd).

<form action="{% url 'shop:add-to-cart' pk=garment.pk %}" method="post">
                        {% csrf_token %}
                        <input class="btn btn-outline-dark mt-auto" type="submit" value="Echanger"/>
</form>

Donc pour toi ici tout est ok rien ne te choque ? je peux procéder de cette manière ?

Biensûr quand j'ai des données comme un password, username ou autre j'envoi ça avec une post non pas dans l'url du coup, mais dans le corps.

Si c'est ok on peut clôturer :)

Je pense que tu as bien compris les concepts de base, maintenant il faut vraiment que tu te mettes dans la tête d'un "attaquant".

Comme je te disais je crois dans un autre fil, le fait de forcer une requête à être en POST ou en GET ne change pas grand chose.

N'importe qui peut faire une requête POST ou GET vers ton site en mettan des informations dans le corps de la requête.

Donc le problème n'est pas le type de requête mais comment après dans ta vue tu gères tout ça.

Si ta vue permet à n'importe qui, qui n'est pas connecté, d'aller ajouter des données ou modifier des données existantes d'un autre utilisateur : là c'est problématique ! Et ce n'est pas le POST qui protégera ça, mais le fait que tu empêches l'accès aux utilisateurs non connectés, aux utilisateurs à qui la ressource n'appartient pas, etc.

Oui d'accord, je pense avoir compris ça j'ai commencé à le faire depuis la dernière question justement :

exemple :

@login_required
@require_POST
def close_ticket(request, pk):
    user = request.user
    ticket = Ticket.objects.get(pk=pk)
    if user != ticket.user and not user.is_superuser:
        raise PermissionDenied()
    ticket.closed = True
    ticket.save()
    return redirect("index")

Pour net RPG je me suis fait une petite fonction que j'appelle pour sécuriser à chaque fois.

from django.core.exceptions import PermissionDenied


def character_permission(game_character, user):
    if game_character.user != user:
        raise PermissionDenied()

Si tu valides c'est compris ! :)

Je valide ! Et une alternative est de créer un décorateur (on rentre dans les sujets plus avancés mais je te laisse ça pour plus tard ou la curiosité) :

Le décorateur doit prendre une vue en argument et retourner une nouvelle vue.

from functools import wraps
from django.core.exceptions import PermissionDenied

def user_has_permission(view_func):
    @wraps(view_func)
    def _wrapped_view(request, *args, **kwargs):
        ticket = Ticket.objects.get(pk=kwargs['pk'])
        if request.user != ticket.user:
            raise PermissionDenied("Vous n'avez pas accès à ce ticket.")
        return view_func(request, *args, **kwargs)
    return _wrapped_view

Et pour l'utiliser sur une vue, tu ferais :

@login_required
@user_has_permission
def close_ticket(request, pk):
    ticket = Ticket.objects.get(pk=pk)
    ticket.closed = True
    ticket.save()
    return redirect("index")

Ok super !

Je vais me poser sur les décorateurs la semaine prochaine alors !

Si vraiment j'y arrive pas j'ouvre une nouvelle question lol

Merci Thibault !

ça me rassure de savoir que je faisais bien les choses alors ouf !

Inscris-toi

(c'est gratuit !)

Inscris-toi

Tu dois créer un compte pour participer aux discussions.