Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,16 @@ Les routes déclarées par le _blueprint_ de `UsersHub-authentification-module`

Les routes suivantes sont implémentés dans `UsersHub-authentification-module`:

| Route URI | Action | Paramètres | Retourne |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------- |
| `/providers` | Retourne l'ensemble des fournisseurs d'identité activés | NA | |
| `/get_current_user` | Retourne les informations de l'utilisateur connecté | NA | {user,expires,token} |
| `/login/<provider>` | Connecte un utilisateur avec le provider <provider> | Optionnel({user,password}) | {user,expires,token} ou redirect |
| `/public_login` | Connecte l'utilisateur permettant l'accès public à votre application | NA | {user,expires,token} |
| `/logout` | Déconnecte l'utilisateur courant | NA | redirect |
| `/authorize` | Connecte un utilisateur à l'aide des infos retournées par le fournisseur d'identité (Si redirection vers un portail de connexion par /login) | {data} | redirect |
| Route URI | Action | Paramètres | Retourne |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------- |
| `/providers` | Retourne l'ensemble des fournisseurs d'identités activés | NA | |
| `/get_current_user` | Retourne les informations de l'utilisateur connecté | NA | {user,expires,token} |
| `/login/<provider>` | Connecte un utilisateur avec le provider <provider> | Optionnel({user,password}) | {user,expires,token} ou redirect |
| `/public_login` | Connecte l'utilisateur permettant l'accès public à votre application | NA | {user,expires,token} |
| `/logout` | Déconnecte l'utilisateur courant | NA | redirect |
| `/authorize` | Connecte un utilisateur à l'aide des infos retournées par le fournisseurs d'identités (Si redirection vers un portail de connexion par /login) | {data} | redirect |

En cas d'erreur d'autorisation, la route `/authorize` redirige vers la page de login avec un paramètre `login_error` (message destiné au frontend).

### Méthodes définies dans le module

Expand Down
63 changes: 56 additions & 7 deletions src/pypnusershub/auth/providers/openid_provider.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from datetime import datetime
from typing import Any, Optional, Tuple, Union

import requests
from flask import Response, current_app, session, url_for
import sqlalchemy as sa
from marshmallow import EXCLUDE, ValidationError, fields
from pypnusershub.auth import Authentication, ProviderConfigurationSchema, oauth
from pypnusershub.auth.user_manager import UserManager
from pypnusershub.db import db, models
from pypnusershub.utils import get_current_app_id
from werkzeug.exceptions import Unauthorized
from enum import Enum

Expand Down Expand Up @@ -38,6 +42,7 @@ class OpenIDProvider(Authentication):
group_claim_name = "groups"
identifier_field = "preferred_username"
reconciliate_attr = "email"
auto_validate_new_user = True

def authenticate(self, *args, **kwargs) -> Union[Response, models.User]:
redirect_uri = url_for(
Expand All @@ -59,21 +64,63 @@ def authorize(self):
UserColumns.EMAIL: user_info["email"],
UserColumns.PRENOM_ROLE: user_info["given_name"],
UserColumns.NOM_ROLE: user_info["family_name"],
UserColumns.ACTIVE: True,
UserColumns.ACTIVE: self.auto_validate_new_user,
}
source_groups = (
user_info[self.group_claim_name]
if self.group_claim_name in user_info
else []
)
user = self.insert_or_update_role(
new_user,
source_groups=source_groups,
reconciliate_attr=self.reconciliate_attr,
fields_to_update=self.fields_to_override,

# Existing user
existing_user = db.session.execute(
sa.select(models.User).where(
getattr(models.User, self.reconciliate_attr)
== new_user[self.reconciliate_attr]
)
).scalar_one_or_none()

# Manual validation: existing users can still log in
# Auto-validation: create/update user and allow login immediately
if existing_user or self.auto_validate_new_user:
user = self.insert_or_update_role(
new_user,
source_groups=source_groups,
reconciliate_attr=self.reconciliate_attr,
fields_to_update=self.fields_to_override,
)
db.session.commit()
return user

# Manual validation: new users create a temp request
# - Avoid creating duplicate pending requests
temp_user_exists = db.session.execute(
sa.select(models.TempUser).where(
getattr(models.TempUser, self.reconciliate_attr)
== new_user[self.reconciliate_attr],
)
).scalar_one_or_none()

if temp_user_exists:
raise Unauthorized(
"Demande de creation de compte en attente de validation."
)

# - create pending request
temp_user = models.TempUser(
token_role=UserManager.generate_token(),
identifiant=new_user["identifiant"],
nom_role=new_user["nom_role"],
prenom_role=new_user["prenom_role"],
email=new_user["email"],
groupe=False,
id_application=get_current_app_id(),
)
db.session.add(temp_user)
db.session.commit()
return user
raise Unauthorized(
"Demande de creation de compte créée et en attente de validation."
)

def revoke(self):
if not "openid_token_resp" in session:
Expand All @@ -99,6 +146,7 @@ class OpenIDProviderConfiguration(ProviderConfigurationSchema):
load_default="preferred_username"
) # Claim d’identification du token OpenID/OIDC
RECONCILIATE_ATTR = fields.String(load_default="email")
AUTO_VALIDATE_NEW_USER = fields.Boolean(load_default=False)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le fonctionnement par défaut est donc modifié (jusque là, le user se créait automatiquement). Toutefois, c'est le fonctionnement par défaut dans la connexion classique AUTO_ACCOUNT_CREATION=false, donc je pense que c'est bien de faire comme tu as fait et qu'il faudra juste le notifier dans le changelog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a la base, c'était une erreur ! J'ai mis faux pour le dev, j'ai juste oublié de remettre à vrai pour la PR.
Je laisse comme ça du coup ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oui, ça me semble bien de le laisser comme ça. Il faut juste mettre à jour la description de la PR où tu dis :

Ajout de l'entrée de config "auto_validate_new_user", vraie par défaut.

En notifiant bien que le comportement à changer pour mettre en cohérence avec celui de l'auth de base.

CODE_CHALLENGE_METHOD = fields.String(
load_default="S256",
validate=fields.validate.OneOf(["plain", "S256"]),
Expand Down Expand Up @@ -137,6 +185,7 @@ class OpenIDProviderConfiguration(ProviderConfigurationSchema):
self.identifier_field = configuration["IDENTIFIER_FIELD"]
self.reconciliate_attr = configuration["RECONCILIATE_ATTR"]
self.fields_to_override = configuration["FIELDS_TO_OVERRIDE"]
self.auto_validate_new_user = configuration["AUTO_VALIDATE_NEW_USER"]


class OpenIDConnectProvider(OpenIDProvider):
Expand Down
14 changes: 12 additions & 2 deletions src/pypnusershub/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import datetime
import logging
from typing import List
from urllib.parse import urljoin
from urllib.parse import urlencode, urljoin

import sqlalchemy as sa
from flask import (
Expand Down Expand Up @@ -216,7 +216,17 @@ def logout():
@routes.route("/authorize/<provider>", methods=["GET", "POST"])
def authorize(provider="local_provider"):
auth_provider = current_app.auth_manager.get_provider(provider)
authorize_result = auth_provider.authorize()
try:
authorize_result = auth_provider.authorize()
except (Unauthorized, Forbidden) as exc:
log.exception("Authorization error for provider %s", provider)
error_description = exc.description or "Unauthorized"
login_url = f"{current_app.config['URL_APPLICATION']}/#/login"
query_params = {
"login_error": error_description,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour l'instant pas de prise en compte dans le frontend de GeoNature de ce querry param, donc, quand on essaye de créer un compte avec AUTO_VALIDATE_NEW_USER à false, ça nous renvoie sur la page sans message de succès ou d'erreur.

Par ailleurs, pour ce cas spécifique, est-ce que c'est vraiment une erreur que l'on veut afficher ? Il semblerait plus logique de mettre un succès ou une info reprennant MyAccount.Message.AdminAccountEmailConfirmation ("Votre demande de création de compte a bien été prise en compte, elle va être évaluée par un administrateur")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah tiens, j'avais oublié de faire la PR. C'est chose faite :)
PnX-SI/GeoNature#3976

J'ai opté pour l'erreur pour se brancher au plus simplement sur la page existante.

Je suis ouvert à d'autres options, mais j'ai peur que ce soit plus/trop impactant

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je regarde ça avec ton autre PR, merci :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bon en effet, ça demande beaucoup de modification, j'ai essayé de le faire et je me suis un peu lancé dans un tunnel ... J'ai fais deux PR en rapport naturalsolutions/geonature#10, et naturalsolutions#1 à voir ce qu'on en fait

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le travail de @christophe-ramet sera merge après le merge de cette branche

}
return redirect(f"{login_url}?{urlencode(query_params)}", code=302)

if isinstance(authorize_result, models.User):
login_user(authorize_result, remember=True)

Expand Down
Loading