From dd407e1b71ced61266c4bbd471d15904f884d1a1 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 28 Nov 2022 14:52:33 +0100 Subject: [PATCH 1/3] franceconnect_data: update DGFIP API fields labels --- .../franceconnect_data/migrations/0001_initial.py | 12 ++++-------- passerelle/apps/franceconnect_data/models.py | 8 ++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/passerelle/apps/franceconnect_data/migrations/0001_initial.py b/passerelle/apps/franceconnect_data/migrations/0001_initial.py index d0199ffd..79781e7c 100644 --- a/passerelle/apps/franceconnect_data/migrations/0001_initial.py +++ b/passerelle/apps/franceconnect_data/migrations/0001_initial.py @@ -48,23 +48,19 @@ class Migration(migrations.Migration): ), ( 'dgfip_username', - models.CharField( - blank=True, max_length=64, null=True, verbose_name='api.impots.gouv.fr username' - ), + models.CharField(blank=True, max_length=64, null=True, verbose_name='DGFIP API Username'), ), ( 'dgfip_password', - models.CharField( - blank=True, max_length=64, null=True, verbose_name='api.impots.gouv.fr password' - ), + models.CharField(blank=True, max_length=64, null=True, verbose_name='DGFIP API Password'), ), ( 'dgfip_scopes', - models.TextField(blank=True, null=True, verbose_name='api.impots.gouv.fr scopes'), + models.TextField(blank=True, null=True, verbose_name='DGFIP API Scopes'), ), ( 'dgfip_id_teleservice', - models.TextField(blank=True, null=True, verbose_name='api.impots.gouv.fr ID_Teleservice'), + models.TextField(blank=True, null=True, verbose_name='DGFIP API ID_Teleservice'), ), ( 'users', diff --git a/passerelle/apps/franceconnect_data/models.py b/passerelle/apps/franceconnect_data/models.py index 5c15ab87..6c653864 100644 --- a/passerelle/apps/franceconnect_data/models.py +++ b/passerelle/apps/franceconnect_data/models.py @@ -62,13 +62,13 @@ class Resource(BaseResource): ), ) - dgfip_username = models.CharField(_('api.impots.gouv.fr username'), max_length=64, blank=True, null=True) + dgfip_username = models.CharField(_('DGFIP API Username'), max_length=64, blank=True, null=True) - dgfip_password = models.CharField(_('api.impots.gouv.fr password'), max_length=64, blank=True, null=True) + dgfip_password = models.CharField(_('DGFIP API Password'), max_length=64, blank=True, null=True) - dgfip_scopes = models.TextField(_('api.impots.gouv.fr scopes'), blank=True, null=True) + dgfip_scopes = models.TextField(_('DGFIP API Scopes'), blank=True, null=True) - dgfip_id_teleservice = models.TextField(_('api.impots.gouv.fr ID_Teleservice'), blank=True, null=True) + dgfip_id_teleservice = models.TextField(_('DGFIP API ID_Teleservice'), blank=True, null=True) log_requests_errors = False -- 2.39.2 From 30a5d9e75aa2649b30a7c40bc84bfe333757097b Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Thu, 24 Nov 2022 11:32:01 +0100 Subject: [PATCH 2/3] franceconnect_data: add field for DGFIP API base url (#71638) --- passerelle/apps/franceconnect_data/fc.py | 10 ++- .../migrations/0003_auto_20221124_1158.py | 22 ++++++ passerelle/apps/franceconnect_data/models.py | 12 ++- tests/test_franceconnect_data.py | 74 +++++++++++++++---- 4 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 passerelle/apps/franceconnect_data/migrations/0003_auto_20221124_1158.py diff --git a/passerelle/apps/franceconnect_data/fc.py b/passerelle/apps/franceconnect_data/fc.py index 9e86107a..b6958040 100644 --- a/passerelle/apps/franceconnect_data/fc.py +++ b/passerelle/apps/franceconnect_data/fc.py @@ -60,9 +60,10 @@ def base64url_decode(input): class FranceConnect: - def __init__(self, session, logger): + def __init__(self, session, logger, dgfip_api_base_url): self.session = session self.logger = logger + self.dgfip_api_base_url = dgfip_api_base_url self.items = [] self.correlation_id = str(uuid.uuid4()) @@ -135,7 +136,7 @@ class FranceConnect: dgfip_response = self.request( 'dgfip token endpoint', 'POST', - 'https://gwfc.impots.gouv.fr/token', + 'token', data=data, auth=(dgfip_username, dgfip_password), ) @@ -162,7 +163,7 @@ class FranceConnect: dgfip_ressource_ir_response = self.request( 'ressource IR endpoint', 'GET', - 'https://gwfc.impots.gouv.fr/impotparticulier/1.0/situations/ir/assiettes/annrev/%s' % annrev, + 'impotparticulier/1.0/situations/ir/assiettes/annrev/%s' % annrev, headers=headers, ) except FranceConnectError as e: @@ -185,8 +186,9 @@ class FranceConnect: def add(self, key, value): self.items.append((key, value)) - def request(self, label, method, url, *args, **kwargs): + def request(self, label, method, endpoint, *args, **kwargs): self.logger.debug('request %s %s args:%s kwargs:%s', label, method, args, kwargs) + url = urllib.parse.urljoin(self.dgfip_api_base_url, endpoint) self.add(label.replace(' ', '_') + '_request', [method, url, args, kwargs]) try: response = getattr(self.session, method.lower())(url, *args, **kwargs) diff --git a/passerelle/apps/franceconnect_data/migrations/0003_auto_20221124_1158.py b/passerelle/apps/franceconnect_data/migrations/0003_auto_20221124_1158.py new file mode 100644 index 00000000..f138ceca --- /dev/null +++ b/passerelle/apps/franceconnect_data/migrations/0003_auto_20221124_1158.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.24 on 2022-11-24 10:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('franceconnect_data', '0002_token'), + ] + + operations = [ + migrations.AddField( + model_name='resource', + name='dgfip_api_base_url', + field=models.URLField( + default='https://gwfc.dgfip.finances.gouv.fr/', + max_length=256, + verbose_name='DGFIP API base URL', + ), + ), + ] diff --git a/passerelle/apps/franceconnect_data/models.py b/passerelle/apps/franceconnect_data/models.py index 6c653864..ea3005ef 100644 --- a/passerelle/apps/franceconnect_data/models.py +++ b/passerelle/apps/franceconnect_data/models.py @@ -62,6 +62,10 @@ class Resource(BaseResource): ), ) + dgfip_api_base_url = models.URLField( + _('DGFIP API base URL'), max_length=256, default='https://gwfc.dgfip.finances.gouv.fr/' + ) + dgfip_username = models.CharField(_('DGFIP API Username'), max_length=64, blank=True, null=True) dgfip_password = models.CharField(_('DGFIP API Password'), max_length=64, blank=True, null=True) @@ -121,7 +125,9 @@ class Resource(BaseResource): return HttpResponseBadRequest('Missing or invalid origin') redirect_uri = self.build_callback_url(request, origin=origin, mode=mode, test=test) - franceconnect = fc.FranceConnect(session=self.requests, logger=self.logger) + franceconnect = fc.FranceConnect( + session=self.requests, logger=self.logger, dgfip_api_base_url=self.dgfip_api_base_url + ) return HttpResponseRedirect( franceconnect.authorization_request( platform=self.fc_platform, @@ -152,7 +158,9 @@ class Resource(BaseResource): if test and not request.user.is_superuser: return HttpResponseBadRequest('Only admin can use test mode.') - franceconnect = fc.FranceConnect(session=self.requests, logger=self.logger) + franceconnect = fc.FranceConnect( + session=self.requests, logger=self.logger, dgfip_api_base_url=self.dgfip_api_base_url + ) redirect_uri = self.build_callback_url(request, origin=origin, mode=mode, test=test) context = { 'origin': origin, diff --git a/tests/test_franceconnect_data.py b/tests/test_franceconnect_data.py index 4484df43..cf1d5600 100644 --- a/tests/test_franceconnect_data.py +++ b/tests/test_franceconnect_data.py @@ -18,11 +18,52 @@ import json from urllib.parse import parse_qs, urlparse, urlunparse import pytest +from django.utils.timezone import now import tests.utils from passerelle.apps.franceconnect_data.models import Resource from tests.test_rsa13 import mock_response +CURRENT_YEAR = now().year + + +USER_INFO_MOCKED_RESPONSES = [ + ['/api/v1/token', {'access_token': 'at-1234', 'id_token': '.e30=.'}], + [ + '/api/v1/userinfo', + { + 'sub': 'sub-1234', + 'given_name': 'John', + 'family_name': 'Doe', + 'birthdate': '2001-04-28', + 'birthplace': '13055', + 'birthcountry': '99100', + 'gender': 'male', + }, + ], +] + + +DGFIP_MOCKED_RESPONSES = USER_INFO_MOCKED_RESPONSES + [ + [ + '/token', + { + 'access_token': 'eyJ4NXQiOi', + 'expires_in': 3600, + 'scope': 'RessourceIRDerniere2', + 'token_type': 'Bearer', + }, + ] +] + +DGFIP_MOCKED_RESPONSES += [ + [ + '/impotparticulier/1.0/situations/ir/assiettes/annrev/%s' % year, + {'rfr': 0, 'revenuBrutGlobal': 0}, + ] + for year in range(CURRENT_YEAR - 3, CURRENT_YEAR) +] + @pytest.fixture def fc(db): @@ -54,21 +95,7 @@ def test_init_request(app, fc): } -@mock_response( - ['/api/v1/token', {'access_token': 'at-1234', 'id_token': '.e30=.'}], - [ - '/api/v1/userinfo', - { - 'sub': 'sub-1234', - 'given_name': 'John', - 'family_name': 'Doe', - 'birthdate': '2001-04-28', - 'birthplace': '13055', - 'birthcountry': '99100', - 'gender': 'male', - }, - ], -) +@mock_response(*USER_INFO_MOCKED_RESPONSES) def test_callback(app, fc): resp = app.get( 'http://testserver/franceconnect-data/test/callback?origin=http%3A%2F%2Ftestserver&code=5678&raise=1' @@ -119,3 +146,20 @@ def test_callback_error(app, fc): error = json.loads(resp.pyquery('#error').text()) assert error assert 'Error in token endpoint response' in resp + + +@mock_response(*DGFIP_MOCKED_RESPONSES) +def test_dgfip_mode(app, fc): + resp = app.get( + 'http://testserver/franceconnect-data/test/callback?origin=http%3A%2F%2Ftestserver&code=5678&raise=1&mode=dgfip' + ) + data = json.loads(resp.pyquery('#data').text()) + assert data + assert 'id' in data + assert data['text'] == 'John Doe né le April 28, 2001' + + resp = app.get('/franceconnect-data/test/data_source?mode=dgfip&id=' + data['id']) + data = resp.json['data'][0] + assert data['dgfip_ir'] + for year in range(CURRENT_YEAR - 3, CURRENT_YEAR): + assert data['dgfip_ir'][str(year)] -- 2.39.2 From 7b766111c916354284c08cd9f12b49a7f74d4ea4 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 28 Nov 2022 15:15:13 +0100 Subject: [PATCH 3/3] translations update --- passerelle/locale/fr/LC_MESSAGES/django.po | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/passerelle/locale/fr/LC_MESSAGES/django.po b/passerelle/locale/fr/LC_MESSAGES/django.po index c5f37083..2451c1be 100644 --- a/passerelle/locale/fr/LC_MESSAGES/django.po +++ b/passerelle/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Passerelle 0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-11-03 16:06-0500\n" +"POT-Creation-Date: 2022-11-28 15:13+0100\n" "PO-Revision-Date: 2022-11-03 22:06+0100\n" "Last-Translator: Frederic Peters \n" "Language: fr\n" @@ -2390,20 +2390,25 @@ msgid "FranceConnect scopes" msgstr "Domaines (« scopes ») pour FranceConnect" #: apps/franceconnect_data/models.py -msgid "api.impots.gouv.fr username" -msgstr "identifiant pour api.impots.gouv.fr" +msgid "DGFIP API base URL" +msgstr "URL de base de l’API" #: apps/franceconnect_data/models.py -msgid "api.impots.gouv.fr password" -msgstr "mot de passe pour api.impots.gouv.fr" +msgid "DGFIP API Username" +msgstr "Identifiant pour l’API DGFIP" #: apps/franceconnect_data/models.py -msgid "api.impots.gouv.fr scopes" -msgstr "Domaines (« scopes ») pour api.impots.gouv.fr" + +msgid "DGFIP API Password" +msgstr "Mot de passe pour l’API DGFIP" #: apps/franceconnect_data/models.py -msgid "api.impots.gouv.fr ID_Teleservice" -msgstr "ID_Teleservice pour api.impots.gouv.fr" +msgid "DGFIP API Scopes" +msgstr "Scopes de l’API DGFIP" + +#: apps/franceconnect_data/models.py +msgid "DGFIP API ID_Teleservice" +msgstr "ID_Teleservice pour l’API DGFIP" #: apps/franceconnect_data/models.py msgid "Data sources through FranceConnect" -- 2.39.2