236 lines
8.4 KiB
Python
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,
|
|
}
|