Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vgrange/peamu #126

Merged
merged 13 commits into from May 27, 2020
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -12,7 +12,7 @@

Vous pouvez personnaliser la configuration Compose en créant [un fichier `.env`](https://docs.docker.com/compose/env-file/) au même niveau que le fichier `README.md`, puis y configurer les variables d'environnement suivantes :

DJANGO_PORT_ON_DOCKER_HOST=8000
DJANGO_PORT_ON_DOCKER_HOST=8080
Copy link
Collaborator

Choose a reason for hiding this comment

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

Je suggère de mettre à jour .envs/dev.env.template avec cette variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Euh je peux me tromper, mais je crois que envs/dev.env.template est un template pour envs/dev.env et non pour .env qui n'a pas de template pour le coup, seulement les 2 lignes dans le README.

Peut-être que tu veux que je créé un .env.template versionné à la racine et simplifie le README en juste cp .env.template .env?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, j'avais mal compris !

Cela étant, je n'ai pas créé de .env mais plutôt modifié le dev.env pour y inclure cette variable. Je trouve ça plus pratique car cela évite d'avoir deux fichiers qui sourcent les variables d'environnement à utiliser en dev (ce qui constitue une source d'erreur potentielle).

Le site devrait fonctionner pleinement grâce au docker-compose-dev.yml, or ce n'est plus le cas car le port par défaut est 8000 et que cela fait planter PEAMU. Voici donc deux propositions :

  • soit on change le port par défaut du conteneur Django (je serais plutôt pour cette option-là car c'est la plus simple),
  • soit on met à jour dev.env.template pour bien y indiquer le port à prendre en compte.

Qu'en penses-tu ?

Copy link
Contributor Author

@dejafait dejafait May 14, 2020

Choose a reason for hiding this comment

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

J'ai du mal à te suivre, pour moi ta suggestion "on change le port par défaut du conteneur Django" est déjà implémentée en fait. Cf dans docker-compose-dev.yml le

    ports:
      - "${DJANGO_PORT_ON_DOCKER_HOST:-8080}:8080"

par ailleurs j'ai tenté de virer le .env et mettre ses deux lignes dans le envs/dev.env mais ça ne se passe pas bien. Le container tente d'utiliser le port 5432 de mon host (et du coup conflicte avec un autre de mes projets) au lieu d'utiliser le 5433 comme il devrait.

AFAIU le .env est le seul moyen pour injecter les variables dans le docker-compose-dev.yml

@celine-m-s

Copy link
Collaborator

Choose a reason for hiding this comment

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

Autant pour moi, j'avais pas vu que tu avais modifié le docker-compose-dev.yml !

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, pas de problème avec la modification de ce port car il concerne un port sur la machine de dev et pas dans le conteneur Docker.

POSTGRES_PORT_ON_DOCKER_HOST=5433

### Lancer le serveur de développement
Expand All @@ -26,6 +26,8 @@ Ou pour utiliser [un débogueur interactif](https://github.com/docker/compose/is

$ docker-compose -f docker-compose-dev.yml run --service-ports django

Une fois votre serveur de développement lancé, vous pouvez accéder au frontend à l'adresse http://localhost:8080/

### Peupler la base de données

$ make populate_db
Expand Down
30 changes: 25 additions & 5 deletions config/settings/base.py
Expand Up @@ -313,12 +313,32 @@
API_INSEE_KEY = os.environ["API_INSEE_KEY"]
API_INSEE_SECRET = os.environ["API_INSEE_SECRET"]

# Pôle emploi.
# Pôle emploi's Emploi Store Dev aka ESD.
# https://www.emploi-store-dev.fr/portail-developpeur/catalogueapi
API_EMPLOI_STORE_KEY = os.environ["API_EMPLOI_STORE_KEY"]
API_EMPLOI_STORE_SECRET = os.environ["API_EMPLOI_STORE_SECRET"]
API_EMPLOI_STORE_AUTH_BASE_URL = "https://entreprise.pole-emploi.fr"
API_EMPLOI_STORE_BASE_URL = "https://api.emploi-store.fr/partenaire"
API_ESD_KEY = os.environ["API_ESD_KEY"]
API_ESD_SECRET = os.environ["API_ESD_SECRET"]
API_ESD_AUTH_BASE_URL = "https://entreprise.pole-emploi.fr"
API_ESD_BASE_URL = "https://api.emploi-store.fr/partenaire"

# PE Connect aka PEAMU - technically one of ESD's APIs.
Copy link
Contributor

Choose a reason for hiding this comment

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

En fait je vois PEAMU écrit partout dans la base de code mais je ne sais pas ce que ça signifie.

Perso je connais PEAM pour "Pôle Emploi Access Management" tel que mentionné dans la doc.

Je suppose que le U vient pour "User" ? Ça serait pas mal de le mentionner une fois en commentaire dans les settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nouveau commentaire :

# PE Connect aka PEAMU - technically one of ESD's APIs.
# PEAM stands for Pôle Emploi Access Management.
# Technically there are two PEAM distinct systems:
# - PEAM "Entreprise", PEAM-E or PEAME for short.
# - PEAM "Utilisateur", PEAM-U or PEAMU for short.
# To avoid confusion between the two when contacting ESD support,
# we get the habit to always explicitely state that we are using PEAM*U*.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pour mémoire j'ai pris cette habitude suite à une grosse galère avec le support ESD pour un souci PE Connect LBB. Après m'avoir baladé d'interlocuteur en interlocuteur, je me suis pris un "Ah bon c'est pas PEAM Entreprise que vous utilisez?". Never again. PEAMU.

# PEAM stands for Pôle Emploi Access Management.
# Technically there are two PEAM distinct systems:
# - PEAM "Entreprise", PEAM-E or PEAME for short.
# - PEAM "Utilisateur", PEAM-U or PEAMU for short.
# To avoid confusion between the two when contacting ESD support,
# we get the habit to always explicitely state that we are using PEAM*U*.
PEAMU_AUTH_BASE_URL = 'https://authentification-candidat.pole-emploi.fr'
SOCIALACCOUNT_PROVIDERS={
"peamu": {
"APP": {
"key": "peamu",
"client_id": API_ESD_KEY,
"secret": API_ESD_SECRET
},
},
}
SOCIALACCOUNT_EMAIL_VERIFICATION = "none"
SOCIALACCOUNT_ADAPTER = "itou.allauth.peamu.adapter.PEAMUSocialAccountAdapter"

# PDFShift
PDFSHIFT_API_KEY = os.environ["PDFSHIFT_API_KEY"]
Expand Down
18 changes: 18 additions & 0 deletions config/settings/dev.py.template
Expand Up @@ -42,3 +42,21 @@ DEBUG_TOOLBAR_CONFIG = {
],
"SHOW_TEMPLATE_CONTEXT": True,
}

# PEAMU.
# ------------------------------------------------------------------------------


# This trick
# https://github.com/pennersr/django-allauth/issues/749#issuecomment-70402595
# fixes the following issue
# https://github.com/pennersr/django-allauth/issues/749
# Without this trick, python manage.py makemigrations
# would want to create a migration in django-allauth dependency
# /usr/local/lib/python3.7/site-packages/allauth/socialaccount/migrations/0004_auto_20200415_1510.py
# - Alter field provider on socialaccount
# - Alter field provider on socialapp
MIGRATION_MODULES = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Si je comprends bien, la migration générée ne concerne pas spécifiquement PEAMU connect mais plutôt aullauth. Je suppose que si demain nous ajoutons un nouveau provider non existant par défaut dans la lib, sans ce réglage, Django voudra générer de nouveau une migration dans la lib.

C'est certainement un peu overkill, mais étant donné que peamu est un type de provider utilisé par django-allauth, j'aurais plutôt créé un dossier allauth contenant peamu et migrations.
Ainsi, on comprend bien que les migrations concernent la lib et non ton module, et que peamu est un nouveau provider.
De plus, si nous sommes amenés à créer un nouveau provider un jour, on saura où le ranger. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok nouvelle structure :
image

'socialaccount': 'itou.allauth.migrations',
}

4 changes: 2 additions & 2 deletions config/settings/test.py
Expand Up @@ -10,8 +10,8 @@
API_BAN_BASE_URL = None
API_INSEE_KEY = None
API_INSEE_SECRET = None
API_EMPLOI_STORE_KEY = None
API_EMPLOI_STORE_SECRET = None
API_ESD_KEY = None
API_ESD_SECRET = None

# Disable logging and traceback in unit tests for readability.
# https://docs.python.org/3/library/logging.html#logging.disable
Expand Down
19 changes: 18 additions & 1 deletion config/urls.py
Expand Up @@ -6,6 +6,7 @@
from itou.utils.urls import SiretConverter
from itou.www.dashboard import views as dashboard_views
from itou.www.signup import views as signup_views
from itou.www.login import views as login_views


register_converter(SiretConverter, "siret")
Expand All @@ -26,6 +27,15 @@
r"^accounts/signup/$", signup_views.signup
),
# --------------------------------------------------------------------------------------
# Override allauth `account_login` URL.
# /accounts/login/ <=> account_login
# We override this view because the login page should look slightly differently
# for job seekers, prescribers and employers.
# Also, PEAMU is only available for job seekers.
re_path(
r"^accounts/login/$", login_views.login
),
# --------------------------------------------------------------------------------------
# Override allauth `account_change_password` URL.
# /accounts/password/change/ <=> account_change_password
# https://github.com/pennersr/django-allauth/issues/468
Expand All @@ -35,9 +45,16 @@
# Avoid user enumeration via password reset page.
re_path(r"^accounts/password/reset/$", signup_views.ItouPasswordResetView.as_view()),
# --------------------------------------------------------------------------------------
# Other allauth URLs.
# Override allauth `account_logout` URL.
# /accounts/logout/ <=> account_logout
# We need custom code to process PEAMU logout.
re_path(r"^accounts/logout/$", dashboard_views.logout),
# -------------------------------------------------------------------------------------- # Other allauth URLs.
path("accounts/", include("allauth.urls")),
# --------------------------------------------------------------------------------------
# PEAMU URLs.
path("accounts/", include("itou.allauth.peamu.urls")),
# --------------------------------------------------------------------------------------

# www.
path("", include("itou.www.home.urls")),
Expand Down
2 changes: 1 addition & 1 deletion docker-compose-dev.yml
Expand Up @@ -34,7 +34,7 @@ services:
- .:/app
restart: always
ports:
- "${DJANGO_PORT_ON_DOCKER_HOST:-8000}:8000"
- "${DJANGO_PORT_ON_DOCKER_HOST:-8080}:8000"

volumes:
postgres_data:
Expand Down
5 changes: 4 additions & 1 deletion docker/dev/django/entrypoint.sh
Expand Up @@ -13,6 +13,9 @@ done
# tail -f /dev/null & wait

django-admin migrate
django-admin runserver_plus 0.0.0.0:8000

# --nopin disables for you the annoying PIN security prompt on the web
# debugger. For local dev only of course!
django-admin runserver_plus 0.0.0.0:8000 --nopin

exec "$@"
4 changes: 2 additions & 2 deletions envs/secrets.env.template
Expand Up @@ -4,7 +4,7 @@ API_INSEE_SECRET=set_it
API_MAILJET_KEY=set_it
API_MAILJET_SECRET=set_it

API_EMPLOI_STORE_KEY=set_it
API_EMPLOI_STORE_SECRET=set_it
API_ESD_KEY=set_it
API_ESD_SECRET=set_it

PDFSHIFT_API_KEY=set_it
2 changes: 2 additions & 0 deletions itou/allauth/migrations/README.md
@@ -0,0 +1,2 @@
This empty folder is necessary for the MIGRATION_MODULES trick.
See dev.py.template for more information.
Empty file added itou/allauth/peamu/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions itou/allauth/peamu/adapter.py
@@ -0,0 +1,32 @@
import requests
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter
from django.conf import settings

from itou.allauth.peamu.provider import PEAMUProvider


class PEAMUSocialAccountAdapter(DefaultSocialAccountAdapter):
def populate_user(self, request, sociallogin, data):
user = super().populate_user(request, sociallogin, data)
setattr(user, "is_job_seeker", True)
return user


class PEAMUOAuth2Adapter(OAuth2Adapter):
provider_id = PEAMUProvider.id

authorize_url = f"{settings.PEAMU_AUTH_BASE_URL}/connexion/oauth2/authorize"
access_token_url = f"{settings.PEAMU_AUTH_BASE_URL}/connexion/oauth2/access_token"
profile_url = f"{settings.API_ESD_BASE_URL}/peconnect-individu/v1/userinfo"
headers = {"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}

def complete_login(self, request, app, token, **kwargs):
id_token = token.token
headers = {"Authorization": f"Bearer {id_token}"}
resp = requests.get(self.profile_url, params=None, headers=headers)
resp.raise_for_status()
extra_data = resp.json()
extra_data["id_token"] = id_token
login = self.get_provider().sociallogin_from_response(request, extra_data)
return login
46 changes: 46 additions & 0 deletions itou/allauth/peamu/client.py
@@ -0,0 +1,46 @@
from urllib.parse import parse_qsl

import requests
from allauth.socialaccount.providers.oauth2.client import OAuth2Client, OAuth2Error


class PEAMUOAuth2Client(OAuth2Client):
"""
Required exclusively for injecting realm=/individu
when requesting access token.
(╯°□°)╯︵ ┻━┻
"""

def get_access_token(self, code):
"""
This whole method is unchanged except for the
`params = {"realm": "/individu"}` hack.
Original code:
https://github.com/pennersr/django-allauth/blob/6a6d3c618ab018234dde8701173093274710ee0a/allauth/socialaccount/providers/oauth2/client.py#L44
"""
data = {"redirect_uri": self.callback_url, "grant_type": "authorization_code", "code": code}
if self.basic_auth:
auth = requests.auth.HTTPBasicAuth(self.consumer_key, self.consumer_secret)
else:
auth = None
data.update({"client_id": self.consumer_key, "client_secret": self.consumer_secret})
params = {"realm": "/individu"}
self._strip_empty_keys(data)
url = self.access_token_url
if self.access_token_method == "GET":
params = data
data = None
resp = requests.request(
self.access_token_method, url, params=params, data=data, headers=self.headers, auth=auth
)

access_token = None
if resp.status_code in [200, 201]:
# Weibo sends json via 'text/plain;charset=UTF-8'
if resp.headers["content-type"].split(";")[0] == "application/json" or resp.text[:2] == '{"':
access_token = resp.json()
else:
access_token = dict(parse_qsl(resp.text))
if not access_token or "access_token" not in access_token:
raise OAuth2Error("Error retrieving access token: %s" % resp.content)
return access_token
83 changes: 83 additions & 0 deletions itou/allauth/peamu/provider.py
@@ -0,0 +1,83 @@
from allauth.socialaccount import providers
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
from django.conf import settings


# We do not use the extra 8 APIs yet, even though they are prepared below,
# because the ESD staff has not validated our 8 contracts yet.
USE_ALL_APIS = False

BASIC_SCOPES = [
# API Se connecter avec Pôle emploi (individu) v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-peconnect-individu-v1.html
"openid",
"api_peconnect-individuv1",
"email",
"profile",
]

EXTRA_SCOPES = [
# API Coordonnées v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-peconnect-coordonnees-v1.html
"api_peconnect-coordonneesv1",
"coordonnees",
# API Statut v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-peconnect-statut-v1.html
"api_peconnect-statutv1",
"statut",
# API Date de naissance v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-peconnect-datenaissance-v1.html
"api_peconnect-datenaissancev1",
"datenaissance",
# API Indemnisations v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-indemnisations-v1.html
"api_peconnect-indemnisationsv1",
"indemnisation",
# API Expériences professionnelles v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-experiences-professionnelles.html
"api_peconnect-experiencesv1",
"pfcexperiences",
# API Expériences déclarées par l’Employeur v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-peconnect-exp-declarees-v1.html
"api_peconnect-experiencesprofessionellesdeclareesparlemployeurv1",
"passeprofessionnel",
# API Formations professionnelles v1
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-formations-professionnelles.html
"api_peconnect-formationsv1",
"pfcformations",
"pfcpermis",
# API Compétences professionnelles v2
# https://www.emploi-store-dev.fr/portail-developpeur-cms/home/catalogue-des-api/documentation-des-api/api/api-pole-emploi-connect/api-peconnect-competence-v2-1.html
"api_peconnect-competencesv2",
"pfccompetences",
"pfclangues",
"pfccentresinteret",
]


class PEAMUProvider(OAuth2Provider):
id = "peamu"
name = "PEAMU"
account_class = ProviderAccount

def get_default_scope(self):
client_id = settings.SOCIALACCOUNT_PROVIDERS["peamu"]["APP"]["client_id"]
scope = [f"application_{client_id}"] + BASIC_SCOPES
if USE_ALL_APIS:
scope += EXTRA_SCOPES
return scope

def get_auth_params(self, request, action):
ret = super().get_auth_params(request, action)
ret["realm"] = "/individu"
return ret

def extract_uid(self, data):
return str(data["sub"])

def extract_common_fields(self, data):
return dict(email=data.get("email"), last_name=data.get("family_name"), first_name=data.get("given_name"))


providers.registry.register(PEAMUProvider)