16 KiB
Examen final — Programmation web en Python (Modules 1 à 5)
Nom / Prénom : BOICHÉ Gauvain Groupe : MCS P 2027.2
Consignes
- Épreuve individuelle, notes de cours autorisées.
- 45 questions fermées (QCM) : une seule réponse correcte par question — entourez la lettre.
- 13 questions ouvertes : répondez dans l'espace prévu ; pour les analyses de code, soyez précis (ligne, cause, correction).
- Contexte de toutes les questions : la gestion d'un parc d'équipements réseau (serveurs, switchs, routeurs, interfaces).
Q1. Que vaut len(set([22, 80, 443, 22, 80])) ?
- b) 3 (set garde les occurences uniques)
Q2. Parmi ces méthodes HTTP, laquelle n'est pas idempotente ?
- a) GET (Selon Wikipédia : "En mathématiques et en informatique, l'idempotence signifie qu'une opération a le même effet qu'on l'applique une ou plusieurs fois.")
Q3. Quel code de statut HTTP indique qu'une ressource a été créée ?
- b) 201
Question ouverte O1 (2 pts). Expliquez en deux phrases la différence entre Internet et le Web.
Réponse : Le "Web" c'est le concept. C'est la "toile" qui relie tout et forme un tout navigable. "Internet" c'est l'infrastructure, le maillage des machines qui fournissent le service. Pour "filer" la métaphore (filer, toile... humour, drôle) "Internet" ce sont les meubles sur lesquels on tisse la "Toile" pour aller de l'un à l'autre.
Q4. (Analyse de code) Que produit l'exécution de cet extrait ?
adresse = ("10.0.0.1", 22)
adresse[1] = 443
- b) Lève une
TypeError(j'ai failli me faire avoir : ce n'est pas une liste ni un dictionnaire mais un tuple. Ca ne se mute pas.)
Q5. Quelle structure Python n'autorise pas les doublons et n'est pas ordonnée ?
- c)
set(cf. question 1)
Q6. Dans une API REST, quelle URL respecte les bonnes pratiques ?
- b)
/equipements(en vrai je vais faire mon chieur : la convention voudrait du 100% anglais)
Q7. (Analyse de code) Qu'affiche print(2 ** 6 - 2) (nombre d'hôtes d'un /26) ?
- b) 62
Question ouverte O2 (4 pts). Écrivez une fonction Python hotes_disponibles(bits_hote) qui renvoie le nombre d'adresses hôtes utilisables d'un sous-réseau, puis donnez le résultat pour un /24.
Réponse :
def hotes_disponibles(bits_hote):
return 2 ** (32 - bits_hote) - 2
# bits_hote = 24
# => 254
Q8. Que renvoie une fonction Python qui ne contient aucun return ?
- c)
None
Q9. À quoi sert l'en-tête HTTP Content-Type: application/json ?
- b) À indiquer le format du corps du message
Q10. Quel format de données est le standard d'échange des API REST modernes ?
- c) JSON
Question ouverte O3 (3 pts). Qu'est-ce que l'idempotence d'une méthode HTTP ? Donnez une méthode idempotente et une non idempotente, en justifiant.
Réponse : J'ai défini en quesiton 2 : Idempotente, en informatique et en maths, c'est l'obtention d'un même résultat quel que soit le nombre de fois où une requête est exécutée. Aussi, "GET" est idempotente en ce que le résultat sera toujours le même à la millionième demande, tandis qu'un DELETE conduira à une erreur dès la deuxième (ressource supprimée : plus d'action à mener), de même qu'un POST (ressource déjà créée, pas de doublon !) ou un PUT (modifications déjà faites). Pour QUERY je ne saurais rien affirmer encore, méthode trop jeune. Mais je la penserais idempotente.
Q11. À quoi sert une clé primaire ?
- b) À identifier de façon unique chaque ligne (colonne ID par convention)
Q12. Une clé étrangère permet de…
- b) créer une relation vers la clé primaire d'une autre table
Q13. (Analyse de code) Qu'affiche cet extrait ?
resultats = [("srv-web-01", "192.168.1.10"), ("sw-access-02", "192.168.1.2")]
print(resultats[1][0])
- c)
sw-access-02(Premier élément du deuxième élément)
Question ouverte O4 (3 pts). Expliquez ce qu'est une clé étrangère et à quoi elle sert, à l'aide d'un exemple equipement / interface.
Réponse : Une clef étrangère est une référence à la clef primaire d'une autre table pour faire des croisements logiques. Dans un exemple "Equipements / Interfaces", une "Interface" aura une clef primaire, mettons ID "1" pour une interface "HTTP" au port "80". Un équipement fera donc appel à la clef étrangère de l'interface, "1" pour l'affecter à son propre équipement. Ainsi les deux tables sauront que l'interface 1 (HTTP, 80) est liée à l'équipement 1 (Serveur, 15.15.15.15, etc). En tout cas ce qui est sûr, c'est qu'une clef étrangère ne vote pas Le Pen.
Q14. En SQL, quelle commande permet de lire des données ?
- b)
SELECT
Q15. Pourquoi utiliser des ? (requête paramétrée) avec sqlite3 ?
- b) Pour éviter les injections SQL (requête préparée)
Q16. « Un équipement possède plusieurs interfaces » : de quel type de relation s'agit-il ?
- b) Un-à-plusieurs (1-N)
Q17. Dans SQLAlchemy 2.0, quel objet gère les opérations add() / commit() ?
- a)
Engine
Question ouverte O5 (4 pts — analyse de code). Identifiez la faille de sécurité de cet extrait et corrigez-la.
hostname = input("hostname ? ")
cur.execute("SELECT * FROM equipement WHERE hostname = '{hostname}'")
Réponse : Ah bah j'en vois plusieurs. La première je dirais Injection de Script Croisé (XSS) et ensuite on ouvre les vannes à tous les résultats sans discernement, puis aucune validation de la string.
hostname = input("hostname ? ").strip()
if not hostname or len(hostname) > 128:
raise ValueError("Hostname invalide.")
cur.execute("SELECT * FROM equipement WHERE hostname = %s", (hostname,))
Q18. Que fait cursor.fetchall() ?
- a) Exécute la requête (The method fetches all (or all remaining) rows of a query result set and returns a list of tuples.)
Q19. Une relation N-N (plusieurs-à-plusieurs) nécessite…
- b) une table de liaison (d'association)
Q20. Quel est un avantage de l'ORM par rapport au SQL brut ?
- b) Il manipule des objets Python et facilite le changement de SGBD
Question ouverte O6 (3 pts — analyse de code). Après ce code, la ligne n'apparaît pas dans la base. Pourquoi et comment corriger ?
conn = sqlite3.connect("inventaire.db")
cur = conn.cursor()
cur.execute("INSERT INTO equipement (hostname, ip, type) VALUES (?, ?, ?)",
("srv-dns-01", "192.168.1.20", "serveur"))
conn.close()
Réponse : SQLite3 c'est comme Git : sans commit, ça reste en stase.
conn = sqlite3.connect("inventaire.db")
cur = conn.cursor()
cur.execute("INSERT INTO equipement (hostname, ip, type) VALUES (?, ?, ?)",
("srv-dns-01", "192.168.1.20", "serveur"))
conn.commit()
conn.close()
Q21. Flask est un…
- a) framework full-stack
Q22. (Analyse de code) Que renvoie cette vue Flask ?
@app.route("/api/statut")
def statut():
return {"etat": "ok"}
- c) Une réponse JSON avec le code 200
Q23. Quel décorateur associe une URL à une fonction en Flask ?
- b)
@app.route
Question ouverte O7 (3 pts). À quoi sert la bibliothèque requests ? Donnez un exemple d'appel GET avec un timeout.
Réponse : Comme le dit la doc, ça envoie des requêtes HTTP/1.1 très facilement.
import requests
req = requests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass'), timeout=10)
Q24. Comment lit-on le corps JSON d'une requête entrante en Flask ?
- b)
request.get_json()
Q25. Avec requests, comment envoyer un corps JSON dans un POST ?
- c)
json=...
Q26. Que fait response.raise_for_status() ?
- b) Lève une exception si le code est ≥ 400
Q27. En Jinja2, quelle syntaxe affiche une variable ?
- b)
{{ variable }}
Question ouverte O8 (4 pts — analyse de code). Un client envoie POST /api/equipements et reçoit 405 Method Not Allowed. Voici la route. Pourquoi ? Corrigez-la.
@app.route("/api/equipements")
def creer_equipement():
donnees = request.get_json()
...
Réponse : La méthode n'est pas donnée. Deux solutions (en soit une pour l'API mais on va faire comme si) :
# 1 : forcer la méthode
@app.route("/api/equipements", methods=["POST"])
def creer_equipement():
donnees = request.get_json()
# 2 : uniquement pour les API, pas de GUI
@app.post("/api/equipements")
def creer_equipement():
donnees = request.get_json()
Q28. En Jinja2, quelle syntaxe permet d'écrire une boucle ?
- b)
{% for e in liste %} … {% endfor %}
Q29. Quel moteur de templates Flask utilise-t-il par défaut ?
Q30. Comment renvoyer un JSON avec le code 201 en Flask ?
- b)
return jsonify(x), 201
Question ouverte O9 (3 pts). Écrivez une route Flask GET /api/equipements/<int:equipement_id> qui renvoie l'équipement en JSON, ou une erreur 404 s'il n'existe pas (liste equipements en mémoire). Expliquez brièvement votre code.
Réponse : Une méthode (issue des cours de cette semaine) qui lit les équipements, compare les ID présents avec l'ID fourni, et retourne l'élément s'il trouve une correspondance. Mais comme tout bon code se passe de commentaire, il est assez clair de lui-même :
@app.route('/api/equipments/<int:equipment_id>', methods=['GET'])
def get_equipment(equipment_id):
equipment = next((e for e in equipments if e["id"] == equipment_id), None)
if equipment is None:
return jsonify({"erreur": "Équipement non trouvé"}), 404
return jsonify(equipment), 200
Q31. Django est un framework…
- b) full-stack (« batteries included »)
Q32. Que signifie l'architecture MTV de Django ?
- a) Model-Template-View
Q33. Quelle commande génère les fichiers de migration ?
- b)
makemigrations
Question ouverte O10 (3 pts). Qu'est-ce qu'une migration Django et pourquoi en a-t-on besoin ? (une analogie est acceptée)
Réponse : Il s'agit d'une propagation des changements. Djangon compare l'ancienne et la nouvelle structure du projet, avec ses applications, et va appliquer les changements idoines dès lors qu'on lui demande. Une analogie que je peux tirer de leur documentation, c'est l'équivalent d'un
git pull origin main. Une fois toutes les merge requests faites, on prend la dernière version de la branche principale.
Q34. Quelle commande applique les migrations à la base de données ?
- b)
migrate
Q35. (Analyse de code) L'équipement 999 n'existe pas. Que se passe-t-il ?
Equipement.objects.get(id=999)
- d) Renvoie une erreur 404
Q36. Dans DRF, le rôle d'un sérialiseur est de…
- b) convertir les objets ↔ JSON et valider les données
Q37. Quelle classe DRF fournit automatiquement tout le CRUD ?
- b)
ModelViewSet
Question ouverte O11 (4 pts — analyse de code). Cette vue déclenche l'erreur « didn't return an HttpResponse object ». Pourquoi ? Corrigez-la.
def liste_equipements(request):
return Equipement.objects.filter(actif=True)
Réponse : Je... ne... vois pas en quoi, j'ai ça et ça marchait :
@login_required
def list_active_equipments(request):
equipments = Equipment.objects.filter(is_active=True)
return render(request, "parc/list.html", {"equipments": equipments})
Ah bah c'est peut-être ça alors. Faire un
returnaussi tôt sans formater avec unrenderdoit renvoyer quelque chose de vide. Et après recherche, c'est bien ça. Il faut un Wrapper autour de cette réponse.
Q38. À quoi sert un DefaultRouter en DRF ?
(La doc : The default router extends the SimpleRouter, but also adds in a default API root view, and adds format suffix patterns to the URLs.)
- b) À générer automatiquement les URLs d'un ViewSet
Q39. La permission IsAuthenticatedOrReadOnly autorise…
- b) la lecture à tous, l'écriture aux utilisateurs authentifiés
Q40. Quel attribut booléen du modèle User donne accès à l'interface /admin/ ?
- c)
is_superuser(https://docs.djangoproject.com/en/6.0/ref/contrib/admin/#:~:text=form%20like%20so%3A-,class%20MyModelAdmin(admin.ModelAdmin)%3A,-def%20get_form()
Question ouverte O12 (4 pts). Écrivez un EquipementSerializer (DRF ModelSerializer) exposant id, hostname, ip, actif, puis expliquez en une phrase la différence entre APIView et ModelViewSet.
Réponse : En résumé : une APIView offre toute possibilité mais demande encore une construction manuelle de la logique des points de terminaisons. Le ModelViewSet offre l'ensemble des méthodes CRUD prêt à l'emploi, même si ça reste personnalisable. En gros on a la V1 et la V2.
class EquipmentSerializer(serializers.ModelSerializer):
class Meta:
model = Equipment
fields = ["id", "hostname", "ip", "is_active"]
Q41. Un utilisateur authentifié accède aux équipements d'un autre en modifiant simplement l'identifiant dans l'URL (/api/equipements/42/). De quel risque OWASP s'agit-il ?
Q42. Dans un ViewSet DRF, quelle technique garantit que chaque utilisateur ne voie que ses ressources ?
- b) Surcharger get_queryset() en filtrant sur request.user
Q43. Pourquoi éviter fields = "all" dans un ModelSerializer exposé par une API ?
- c) Cela peut exposer des champs sensibles et autoriser le mass assignment (API3 BOPLA)
Q44. Une API récupère une URL fournie par l'utilisateur (webhook). Quelle est la meilleure protection contre la SSRF (API7) ?
- c) Valider le schéma puis bloquer les IP privées/loopback/link-local (ou utiliser une allowlist) (j'ai hésité avec la B) mais ça semble bien être ça (https://owasp.org/API-Security/editions/2023/en/0xa7-server-side-request-forgery/))
Q45. Quel code de statut HTTP DRF renvoie-t-il lorsqu'un client dépasse la limite de débit (throttling, API4) ?
- c) 429 Too Many Requests
Question ouverte O13 - Trouver et corriger la faille (analyse de code) Le développeur veut renvoyer « mes équipements ». Voici sa vue DRF. Identifiez la faille, nommez le risque OWASP, expliquez l'impact, puis corrigez le code.
# parc/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Equipement
from .serializers import EquipementSerializer
@api_view(["GET"])
def mes_equipements(request):
user_id = request.query_params.get("user_id") # ex. /api/mes-equipements/?user_id=1
equipements = Equipement.objects.filter(proprietaire_id=user_id)
return Response(EquipementSerializer(equipements, many=True).data)
Réponse : J'ai mis une petite minute à trouver quand même, mais je pense l'avoir : on peut itérer comme on veut sur chaque ID et avoir tout l'inventaire sans besoin de s'authentifier. Ca ne compare jamais son PROPRE identifiant (si applicable) pour avoir SON parc d'équipements. C'est un OWASP API1, un BOLA. N'importe qui peut faire son organigramme d'équipements dans l'orga, c'est pratique pour un pirate. Je vais juste reprendre mon propre code en correction :
from rest_framework.permissions import IsAuthenticated
@api_view(["GET"])
def mes_equipements(request):
permission_classes = [IsAuthenticated]
equipements = Equipement.objects.filter(proprietaire_id=request.user)
return Response(EquipementSerializer(equipements, many=True).data)
FIN DU DOCUMENT