Files
ctp_blogr/ctp_blogr/views/default.py
T
2026-10-06 16:23:08 +02:00

236 lines
8.4 KiB
Python

import datetime
from authlib.common.security import generate_token
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPForbidden, HTTPFound
from pyramid.security import remember, forget
from ..services.google_oauth import GoogleOAuthError
from ..services.user import UserService
from ..services.blog_record import BlogRecordService
from ..forms import UserEditForm
@view_config(route_name='home', renderer='../templates/home.jinja2')
def home(request):
# get the last created posts
last_ten = BlogRecordService.get_last_created(request)
# get Chappatte cartoon
entry = BlogRecordService.by_id(request, 95)
return {
'page_title': "This is the Way",
'last_ten': last_ten,
'cartoon_title': entry.title,
'cartoon_url': entry.body,
}
@view_config(route_name='apropos',
renderer='../templates/apropos.jinja2')
def apropos(request):
return {
'page_title': "A propos",
}
@view_config(route_name='login', renderer='../templates/login.jinja2')
def login(request):
login_url = request.route_url('login')
if 'form.submitted' in request.params:
session = request.session
state = generate_token(32)
nonce = generate_token(32)
code_verifier = generate_token(48)
session['oauth_state'] = state
session['oauth_nonce'] = nonce
session['oauth_code_verifier'] = code_verifier
import pdb;pdb.set_trace()
url = request.registry.google_oauth.create_authorization_url(
redirect_uri=request.route_url('login_google_callback'),
state=state,
nonce=nonce,
code_verifier=code_verifier,
)
return HTTPFound(location=url)
return {
'page_title': "",
'login_url': login_url,
}
@view_config(context=HTTPForbidden,
renderer='../templates/denied.jinja2')
def denied(request):
# Vrai 403. Un forbidden_view_config sur la page de connexion rendait le
# statut 200 et laissait croire que la connexion locale etait possible.
request.response.status_int = 403
return {
'page_title': "Accès refusé",
'message': getattr(request, 'denied_message', None)
or "Vous n'avez pas les droits nécessaires pour cette page.",
}
@view_config(route_name='login_google_callback',
renderer='../templates/denied.jinja2')
def login_google_callback(request):
session = request.session
def forget_oauth_values():
session.pop('oauth_state', None)
session.pop('oauth_nonce', None)
session.pop('oauth_code_verifier', None)
def back_to_login(message):
forget_oauth_values()
request.session.flash(message, 'danger')
return HTTPFound(location=request.route_url('login'))
if request.params.get('error'):
return back_to_login("Connexion Google annulée ou refusée.")
# state, nonce et code_verifier sont consommes AVANT l'echange du code, donc
# un rejeu de la requete ne trouve plus de state et n'atteint pas Google.
state = session.pop('oauth_state', None)
nonce = session.pop('oauth_nonce', None)
code_verifier = session.pop('oauth_code_verifier', None)
if not state or state != request.params.get('state'):
return back_to_login("Requête de connexion invalide ou déjà utilisée.")
if not code_verifier or not nonce:
return back_to_login("Requête de connexion incomplète.")
code = request.params.get('code')
if not code:
return back_to_login("Requête de connexion incomplète.")
# A partir d'ici le code d'autorisation peut avoir ete consomme par Google,
# alors qu'un rejeu repart du COOKIE D'ORIGINE, qui porte encore oauth_state
# et renvoierait le meme code une deuxieme fois, en invalid_grant. On se
# declare donc derniere tentative avant le moindre appel reseau.
attempt = request.environ.get('retry.attempt')
if attempt is not None:
request.environ['retry.attempts'] = attempt + 1
try:
claims = request.registry.google_oauth.fetch_claims(
code=code,
code_verifier=code_verifier,
redirect_uri=request.route_url('login_google_callback'),
nonce=nonce,
)
except GoogleOAuthError as exc:
return back_to_login("Connexion Google échouée : %s" % exc)
email = (claims.get('email') or '').strip()
google_sub = (claims.get('sub') or '').strip()
# `email_verified` vaut False pour un compte Google dont l'adresse n'a pas
# ete verifiee. Sans ce refus, un compte non verifie pourrait se presenter
# avec une adresse de la liste blanche : l'identite EST l'adresse, donc
# accepter une adresse non verifiee equivaut a usurper un compte autorise.
if claims.get('email_verified') is not True:
return back_to_login("L'adresse Google renvoyée n'est pas vérifiée.")
# Sans `sub` on ne peut pas relier la session a un compte Google precis :
# la ligne authorizee se retrouverait avec un google_sub vide, donc
# rebindable par n'importe quel autre compte portant la meme adresse.
if not google_sub:
return back_to_login("Réponse Google incomplète.")
# Liste blanche : la table users fait foi. Aucune ligne n'est creee pour un
# compte inconnu.
user = UserService.by_google_sub(request, google_sub)
if user is None and email:
user = UserService.by_name(request, email)
if user is None:
# 403 explicite, sans lever HTTPForbidden, sinon on retombe sur le
# gabarit de connexion et l'appelant ne voit qu'un 200.
request.response.status_int = 403
return {
'page_title': "Accès refusé",
'message': ("Le compte Google %s n'est pas autorisé sur ce blog. "
"Demandez son ajout à un administrateur." % (email or google_sub)),
}
if user.google_sub != google_sub:
user.google_sub = google_sub
user.last_logged = datetime.datetime.now()
headers = remember(request, user.name)
request.session.flash("Bienvenue %s !" % user.name, 'success')
return HTTPFound(location=request.route_url('home'), headers=headers)
@view_config(route_name='logout', renderer='string')
def logout(request):
username = request.authenticated_userid
if username == None:
username = ''
headers = forget(request)
request.session.flash('Au revoir ' + username + ' !', 'success')
return HTTPFound(location=request.route_url('home'), headers=headers)
@view_config(route_name='users', renderer='../templates/users.jinja2', permission='manage')
def users(request):
# get all users
users = UserService.all(request)
return {
'page_title': "Liste des utilisateurs",
'users': users
}
@view_config(route_name='user_edit',
renderer='ctp_blogr:templates/user_edit.jinja2',
permission='manage')
def user_edit(request):
# 'manage' et non 'view' : avec 'view', n'importe quel utilisateur
# connecte pouvait ouvrir /user_edit/<un-autre-email> et le modifier,
# ou /user_edit/0 pour creer une fiche.
message = ''
name = request.matchdict['name']
url = request.route_url('user_edit', name=name)
if 'group:administrators' in request.effective_principals:
url_retour = request.route_url('users')
else:
url_retour = request.route_url('home')
user = UserService.by_name(request, name)
if not user:
# '0' n'existe plus : la création d'un utilisateur a disparu avec
# le mot de passe.
request.session.flash("Utilisateur non trouvé : %s" % name, 'danger')
return HTTPFound(location=url_retour)
form = UserEditForm(request.POST, user)
page_title = "Modification utilisateur"
if 'form.submitted' in request.params and form.validate():
del form.name # SECURITY: prevent overwriting of primary key
form.populate_obj(user)
request.session.flash("La fiche a été modifiée avec succès.", 'success')
return HTTPFound(location=url_retour)
if 'form.deleted' in request.params:
UserService.delete(request, user.id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=url_retour)
return {
'page_title': page_title,
'message': message,
'form': form,
'url': url,
'url_retour': url_retour,
'name': name,
}