c2cgeoportal-geoportal 2.7.1.156__py2.py3-none-any.whl → 2.8.1.180__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. c2cgeoportal_geoportal/__init__.py +24 -14
  2. c2cgeoportal_geoportal/lib/authentication.py +10 -14
  3. c2cgeoportal_geoportal/lib/caching.py +8 -6
  4. c2cgeoportal_geoportal/lib/checker.py +10 -6
  5. c2cgeoportal_geoportal/lib/common_headers.py +5 -8
  6. c2cgeoportal_geoportal/lib/dbreflection.py +8 -8
  7. c2cgeoportal_geoportal/lib/filter_capabilities.py +5 -1
  8. c2cgeoportal_geoportal/lib/lingua_extractor.py +11 -12
  9. c2cgeoportal_geoportal/lib/loader.py +1 -1
  10. c2cgeoportal_geoportal/lib/oauth2.py +217 -100
  11. c2cgeoportal_geoportal/lib/wmstparsing.py +8 -12
  12. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +9 -11
  13. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/development.ini +1 -1
  14. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +0 -2
  15. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/requirements.txt +1 -1
  16. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.api.js +6 -4
  17. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +1 -3
  18. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.commons.js +1 -0
  19. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +1 -6
  20. c2cgeoportal_geoportal/scaffolds/advance_update/{{cookiecutter.project}}/geoportal/CONST_Makefile +0 -20
  21. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +20 -6
  22. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/update_l10n.yaml +4 -3
  23. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +22 -22
  24. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +58 -2
  25. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +48 -24
  26. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +2 -5
  27. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/docker-compose-check +25 -0
  28. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +1 -1
  29. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-db.yaml +26 -0
  30. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +53 -26
  31. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-qgis.yaml +23 -0
  32. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.override.sample.yaml +0 -1
  33. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.yaml +3 -3
  34. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +21 -2
  35. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +9 -0
  36. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +38 -14
  37. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/Readme.txt +2 -2
  38. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.conf +15 -0
  39. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.map.tmpl +2 -3
  40. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Landscape.jrxml +5 -0
  41. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Portrait.jrxml +5 -0
  42. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Landscape.jrxml +5 -0
  43. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Portrait.jrxml +5 -0
  44. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/config.yaml.tmpl +6 -0
  45. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/pyproject.toml +4 -0
  46. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/run_alembic.sh +3 -5
  47. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +1 -1
  48. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +1 -1
  49. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/spell-ignore-words.txt +2 -0
  50. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/__init__.py +0 -0
  51. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/test_app.py +38 -0
  52. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/.upgrade.yaml +2 -132
  53. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +210 -1097
  54. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_create_template/tests/test_testapp.py +48 -0
  55. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +17 -15
  56. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +46 -2
  57. c2cgeoportal_geoportal/scripts/c2cupgrade.py +1 -2
  58. c2cgeoportal_geoportal/scripts/pcreate.py +8 -10
  59. c2cgeoportal_geoportal/scripts/theme2fts.py +58 -3
  60. c2cgeoportal_geoportal/views/__init__.py +1 -3
  61. c2cgeoportal_geoportal/views/dynamic.py +1 -1
  62. c2cgeoportal_geoportal/views/entry.py +2 -10
  63. c2cgeoportal_geoportal/views/fulltextsearch.py +1 -1
  64. c2cgeoportal_geoportal/views/geometry_processing.py +3 -3
  65. c2cgeoportal_geoportal/views/layers.py +10 -11
  66. c2cgeoportal_geoportal/views/login.py +63 -8
  67. c2cgeoportal_geoportal/views/mapserverproxy.py +2 -3
  68. c2cgeoportal_geoportal/views/ogcproxy.py +6 -2
  69. c2cgeoportal_geoportal/views/pdfreport.py +4 -4
  70. c2cgeoportal_geoportal/views/printproxy.py +2 -2
  71. c2cgeoportal_geoportal/views/profile.py +1 -1
  72. c2cgeoportal_geoportal/views/proxy.py +2 -4
  73. c2cgeoportal_geoportal/views/raster.py +2 -2
  74. c2cgeoportal_geoportal/views/resourceproxy.py +1 -1
  75. c2cgeoportal_geoportal/views/shortener.py +1 -2
  76. c2cgeoportal_geoportal/views/theme.py +97 -63
  77. c2cgeoportal_geoportal/views/tinyowsproxy.py +3 -12
  78. c2cgeoportal_geoportal/views/vector_tiles.py +1 -1
  79. {c2cgeoportal_geoportal-2.7.1.156.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/METADATA +21 -15
  80. {c2cgeoportal_geoportal-2.7.1.156.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/RECORD +96 -90
  81. {c2cgeoportal_geoportal-2.7.1.156.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/entry_points.txt +1 -0
  82. tests/__init__.py +3 -2
  83. tests/test_cachebuster.py +3 -3
  84. tests/test_caching.py +7 -7
  85. tests/test_checker.py +1 -1
  86. tests/test_decimaljson.py +1 -1
  87. tests/test_headerstween.py +1 -1
  88. tests/test_i18n.py +1 -1
  89. tests/test_init.py +14 -15
  90. tests/test_locale_negociator.py +4 -4
  91. tests/test_mapserverproxy_route_predicate.py +1 -2
  92. tests/test_raster.py +15 -15
  93. tests/test_wmstparsing.py +10 -10
  94. tests/xmlstr.py +1 -3
  95. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/tools/extract-messages.js +0 -41
  96. {c2cgeoportal_geoportal-2.7.1.156.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/WHEEL +0 -0
  97. {c2cgeoportal_geoportal-2.7.1.156.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/top_level.txt +0 -0
@@ -27,14 +27,15 @@
27
27
 
28
28
  import logging
29
29
  from datetime import datetime, timedelta
30
- from typing import Any, Dict, List, Union
30
+ from typing import Any, Dict, List, Optional, Union
31
31
 
32
32
  import basicauth
33
33
  import oauthlib.common
34
34
  import oauthlib.oauth2
35
35
  import pyramid.threadlocal
36
+ from pyramid.httpexceptions import HTTPBadRequest
36
37
 
37
- import c2cgeoportal_commons # pylint: disable=unused-import
38
+ import c2cgeoportal_commons
38
39
  from c2cgeoportal_geoportal.lib.caching import get_region
39
40
 
40
41
  LOG = logging.getLogger(__name__)
@@ -48,7 +49,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
48
49
  # in minutes
49
50
  self.authorization_expires_in = authorization_expires_in
50
51
 
51
- def authenticate_client( # pylint: disable=no-self-use,useless-suppression
52
+ def authenticate_client(
52
53
  self,
53
54
  request: oauthlib.common.Request,
54
55
  *args: Any,
@@ -65,7 +66,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
65
66
  both body and query can be obtained by direct attribute access, i.e.
66
67
  request.client_id for client_id in the URL query.
67
68
 
68
- Arguments:
69
+ Keyword Arguments:
69
70
 
70
71
  request: oauthlib.common.Request
71
72
 
@@ -85,7 +86,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
85
86
 
86
87
  raise NotImplementedError("Not implemented, the method `authenticate_client_id` should be used.")
87
88
 
88
- def authenticate_client_id( # pylint: disable=no-self-use,useless-suppression
89
+ def authenticate_client_id(
89
90
  self,
90
91
  client_id: str,
91
92
  request: oauthlib.common.Request,
@@ -133,7 +134,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
133
134
  LOG.debug("authenticate_client_id => %s", request.client is not None)
134
135
  return request.client is not None
135
136
 
136
- def client_authentication_required( # pylint: disable=no-self-use,useless-suppression
137
+ def client_authentication_required(
137
138
  self,
138
139
  request: oauthlib.common.Request,
139
140
  *args: Any,
@@ -153,7 +154,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
153
154
  client credentials or whenever Client provided client authentication, see
154
155
  `Section 6`_
155
156
 
156
- Arguments:
157
+ Keyword Arguments:
157
158
 
158
159
  request: oauthlib.common.Request
159
160
 
@@ -174,7 +175,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
174
175
 
175
176
  return False
176
177
 
177
- def confirm_redirect_uri( # pylint: disable=no-self-use,useless-suppression
178
+ def confirm_redirect_uri(
178
179
  self,
179
180
  client_id: str,
180
181
  code: str,
@@ -195,7 +196,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
195
196
  the client's allowed redirect URIs, but against the URI used when the
196
197
  code was saved.
197
198
 
198
- Arguments:
199
+ Keyword Arguments:
199
200
 
200
201
  client_id: Unicode client identifier
201
202
  code: Unicode authorization_code.
@@ -226,33 +227,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
226
227
  LOG.debug("confirm_redirect_uri => %s", authorization_code is not None)
227
228
  return authorization_code is not None
228
229
 
229
- def get_code_challenge_method( # pylint: disable=no-self-use,useless-suppression
230
- self, code: str, request: oauthlib.common.Request
231
- ) -> None:
232
- """
233
- Is called during the "token" request processing.
234
-
235
- When a ``code_verifier`` and a ``code_challenge`` has
236
- been provided. See ``.get_code_challenge``. Must return ``plain`` or ``S256``. You can return a custom
237
- value if you have implemented your own ``AuthorizationCodeGrant`` class.
238
-
239
- Arguments:
240
-
241
- code: Authorization code.
242
- request: OAuthlib request.
243
-
244
- Returns: code_challenge_method string
245
-
246
- Method is used by:
247
- - Authorization Code Grant - when PKCE is active
248
- """
249
- del code, request
250
-
251
- LOG.debug("get_code_challenge_method")
252
-
253
- raise NotImplementedError("Not implemented.")
254
-
255
- def get_default_redirect_uri( # pylint: disable=no-self-use,useless-suppression
230
+ def get_default_redirect_uri(
256
231
  self,
257
232
  client_id: str,
258
233
  request: oauthlib.common.Request,
@@ -262,7 +237,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
262
237
  """
263
238
  Get the default redirect URI for the client.
264
239
 
265
- Arguments:
240
+ Keyword Arguments:
266
241
 
267
242
  client_id: Unicode client identifier
268
243
  request: The HTTP Request
@@ -279,7 +254,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
279
254
 
280
255
  raise NotImplementedError("Not implemented.")
281
256
 
282
- def get_default_scopes( # pylint: disable=no-self-use,useless-suppression
257
+ def get_default_scopes(
283
258
  self,
284
259
  client_id: str,
285
260
  request: oauthlib.common.Request,
@@ -289,7 +264,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
289
264
  """
290
265
  Get the default scopes for the client.
291
266
 
292
- Arguments:
267
+ Keyword Arguments:
293
268
 
294
269
  client_id: Unicode client identifier
295
270
  request: The HTTP Request
@@ -308,7 +283,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
308
283
 
309
284
  return ["geomapfish"]
310
285
 
311
- def get_original_scopes( # pylint: disable=no-self-use,useless-suppression
286
+ def get_original_scopes(
312
287
  self,
313
288
  refresh_token: str,
314
289
  request: oauthlib.common.Request,
@@ -318,7 +293,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
318
293
  """
319
294
  Get the list of scopes associated with the refresh token.
320
295
 
321
- Arguments:
296
+ Keyword Arguments:
322
297
 
323
298
  refresh_token: Unicode refresh token
324
299
  request: The HTTP Request
@@ -334,7 +309,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
334
309
 
335
310
  return []
336
311
 
337
- def introspect_token( # pylint: disable=no-self-use,useless-suppression
312
+ def introspect_token(
338
313
  self,
339
314
  token: str,
340
315
  token_type_hint: str,
@@ -366,7 +341,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
366
341
  efficiency, but must fallback to other types to be compliant with RFC.
367
342
  The dict of claims is added to request.token after this method.
368
343
 
369
- Arguments:
344
+ Keyword Arguments:
370
345
 
371
346
  token: The token string.
372
347
  token_type_hint: access_token or refresh_token.
@@ -384,7 +359,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
384
359
 
385
360
  raise NotImplementedError("Not implemented.")
386
361
 
387
- def invalidate_authorization_code( # pylint: disable=no-self-use,useless-suppression
362
+ def invalidate_authorization_code(
388
363
  self,
389
364
  client_id: str,
390
365
  code: str,
@@ -395,7 +370,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
395
370
  """
396
371
  Invalidate an authorization code after use.
397
372
 
398
- Arguments:
373
+ Keyword Arguments:
399
374
 
400
375
  client_id: Unicode client identifier
401
376
  code: The authorization code grant (request.code).
@@ -419,7 +394,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
419
394
  .one()
420
395
  )
421
396
 
422
- def is_within_original_scope( # pylint: disable=no-self-use,useless-suppression
397
+ def is_within_original_scope(
423
398
  self,
424
399
  request_scopes: List[str],
425
400
  refresh_token: str,
@@ -438,7 +413,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
438
413
  used in situations where returning all valid scopes from the
439
414
  get_original_scopes is not practical.
440
415
 
441
- Arguments:
416
+ Keyword Arguments:
442
417
 
443
418
  request_scopes: A list of scopes that were requested by client
444
419
  refresh_token: Unicode refresh_token
@@ -453,7 +428,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
453
428
 
454
429
  return False
455
430
 
456
- def revoke_token( # pylint: disable=no-self-use,useless-suppression
431
+ def revoke_token(
457
432
  self,
458
433
  token: str,
459
434
  token_type_hint: str,
@@ -464,7 +439,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
464
439
  """
465
440
  Revoke an access or refresh token.
466
441
 
467
- Arguments:
442
+ Keyword Arguments:
468
443
 
469
444
  token: The token string.
470
445
  token_type_hint: access_token or refresh_token.
@@ -479,9 +454,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
479
454
 
480
455
  raise NotImplementedError("Not implemented.")
481
456
 
482
- def rotate_refresh_token( # pylint: disable=no-self-use,useless-suppression
483
- self, request: oauthlib.common.Request
484
- ) -> bool:
457
+ def rotate_refresh_token(self, request: oauthlib.common.Request) -> bool:
485
458
  """
486
459
  Determine whether to rotate the refresh token. Default, yes.
487
460
 
@@ -489,7 +462,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
489
462
  or replaced with a new one (rotated). Return True to rotate and
490
463
  and False for keeping original.
491
464
 
492
- Arguments:
465
+ Keyword Arguments:
493
466
 
494
467
  request: oauthlib.common.Request
495
468
 
@@ -502,7 +475,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
502
475
 
503
476
  return True
504
477
 
505
- def save_authorization_code( # pylint: disable=no-self-use,useless-suppression
478
+ def save_authorization_code(
506
479
  self,
507
480
  client_id: str,
508
481
  code: Dict[str, str],
@@ -523,13 +496,13 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
523
496
  The 'code' argument is actually a dictionary, containing at least a
524
497
  'code' key with the actual authorization code:
525
498
 
526
- {'code': 'sdf345jsdf0934f'}
499
+ {'code': '<secret>'}
527
500
 
528
501
  It may also have a 'state' key containing a nonce for the client, if it
529
502
  chose to send one. That value should be saved and used in
530
503
  'validate_code'.
531
504
 
532
- Arguments:
505
+ Keyword Arguments:
533
506
 
534
507
  client_id: Unicode client identifier
535
508
  code: A dict of the authorization code grant and, optionally, state.
@@ -537,6 +510,11 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
537
510
 
538
511
  Method is used by:
539
512
  - Authorization Code Grant
513
+
514
+ To support PKCE, you MUST associate the code with:
515
+
516
+ Code Challenge (request.code_challenge) and
517
+ Code Challenge Method (request.code_challenge_method)
540
518
  """
541
519
  del args, kwargs
542
520
 
@@ -544,7 +522,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
544
522
 
545
523
  from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
546
524
 
547
- user = pyramid.threadlocal.get_current_request().user_
525
+ user = pyramid.threadlocal.get_current_request().user
548
526
 
549
527
  # Don't allows to have two authentications for the same user and the same client
550
528
  authorization_code = (
@@ -555,20 +533,32 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
555
533
  )
556
534
 
557
535
  if authorization_code is not None:
558
- authorization_code.code = code["code"]
559
536
  authorization_code.expire_at = datetime.now() + timedelta(minutes=self.authorization_expires_in)
560
- authorization_code.redirect_uri = request.redirect_uri
561
537
  else:
562
538
  authorization_code = static.OAuth2AuthorizationCode()
563
539
  authorization_code.client_id = request.client.id
564
- authorization_code.code = code["code"]
565
540
  authorization_code.user_id = user.id
566
541
  authorization_code.expire_at = datetime.now() + timedelta(minutes=self.authorization_expires_in)
567
- authorization_code.redirect_uri = request.redirect_uri
542
+ authorization_code.state = code.get("state")
543
+
544
+ authorization_code.code = code["code"]
545
+ authorization_code.redirect_uri = request.redirect_uri
546
+
547
+ client = (
548
+ DBSession.query(static.OAuth2Client)
549
+ .filter(static.OAuth2Client.client_id == client_id)
550
+ .one_or_none()
551
+ )
552
+ if client and client.state_required and not code.get("state"):
553
+ raise HTTPBadRequest("Client is missing the state parameter.")
554
+
555
+ if client and client.pkce_required:
556
+ authorization_code.challenge = request.code_challenge
557
+ authorization_code.challenge_method = request.code_challenge_method
568
558
 
569
559
  DBSession.add(authorization_code)
570
560
 
571
- def save_bearer_token( # pylint: disable=no-self-use,useless-suppression
561
+ def save_bearer_token(
572
562
  self,
573
563
  token: Dict[str, Union[str, int]],
574
564
  request: oauthlib.common.Request,
@@ -589,17 +579,17 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
589
579
 
590
580
  {
591
581
  'token_type': 'Bearer',
592
- 'access_token': 'askfjh234as9sd8',
582
+ 'access_token': '<secret>',
593
583
  'expires_in': 3600,
594
584
  'scope': 'string of space separated authorized scopes',
595
- 'refresh_token': '23sdf876234', # if issued
585
+ 'refresh_token': '<secret>', # if issued
596
586
  'state': 'given_by_client', # if supplied by client
597
587
  }
598
588
 
599
589
  Note that while "scope" is a string-separated list of authorized scopes,
600
590
  the original list is still available in request.scopes
601
591
 
602
- Arguments:
592
+ Keyword Arguments:
603
593
 
604
594
  client_id: Unicode client identifier
605
595
  token: A Bearer token dict
@@ -627,21 +617,18 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
627
617
  .one_or_none()
628
618
  )
629
619
 
630
- if bearer_token is not None:
631
- bearer_token.access_token = token["access_token"]
632
- bearer_token.refresh_token = token["refresh_token"]
633
- bearer_token.expire_at = datetime.now() + timedelta(seconds=float(token["expires_in"]))
634
- else:
620
+ if bearer_token is None:
635
621
  bearer_token = static.OAuth2BearerToken()
636
622
  bearer_token.client_id = request.client.id
637
623
  bearer_token.user_id = request.user.id
638
- bearer_token.access_token = token["access_token"]
639
- bearer_token.refresh_token = token["refresh_token"]
640
- bearer_token.expire_at = datetime.now() + timedelta(seconds=float(token["expires_in"]))
641
-
642
624
  DBSession.add(bearer_token)
643
625
 
644
- def validate_bearer_token( # pylint: disable=no-self-use,useless-suppression
626
+ bearer_token.access_token = token["access_token"]
627
+ bearer_token.refresh_token = token["refresh_token"]
628
+ bearer_token.expire_at = datetime.now() + timedelta(seconds=float(token["expires_in"]))
629
+ bearer_token.state = token.get("state")
630
+
631
+ def validate_bearer_token(
645
632
  self,
646
633
  token: str,
647
634
  scopes: List[str],
@@ -650,7 +637,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
650
637
  """
651
638
  Ensure the Bearer token is valid and authorized access to scopes.
652
639
 
653
- Arguments:
640
+ Keyword Arguments:
654
641
 
655
642
  token: A string of random characters.
656
643
  scopes: A list of scopes associated with the protected resource.
@@ -686,7 +673,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
686
673
  one provided for django these attributes will be made available
687
674
  in all protected views as keyword arguments.
688
675
 
689
- Arguments:
676
+ Keyword Arguments:
690
677
 
691
678
  token: Unicode Bearer token
692
679
  scopes: List of scopes (defined by you)
@@ -705,6 +692,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
705
692
 
706
693
  bearer_token = (
707
694
  DBSession.query(static.OAuth2BearerToken)
695
+ .join(static.User)
708
696
  .filter(static.OAuth2BearerToken.access_token == token)
709
697
  .filter(static.OAuth2BearerToken.expire_at > datetime.now())
710
698
  ).one_or_none()
@@ -715,7 +703,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
715
703
  LOG.debug("validate_bearer_token => %s", bearer_token is not None)
716
704
  return bearer_token is not None
717
705
 
718
- def validate_client_id( # pylint: disable=no-self-use,useless-suppression
706
+ def validate_client_id(
719
707
  self,
720
708
  client_id: str,
721
709
  request: oauthlib.common.Request,
@@ -729,7 +717,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
729
717
  to set request.client to the client object associated with the
730
718
  given client_id.
731
719
 
732
- Arguments:
720
+ Keyword Arguments:
733
721
 
734
722
  client_id: Unicode client identifier
735
723
  request: oauthlib.common.Request
@@ -753,7 +741,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
753
741
  request.client = client
754
742
  return client is not None
755
743
 
756
- def validate_code( # pylint: disable=no-self-use,useless-suppression
744
+ def validate_code(
757
745
  self,
758
746
  client_id: str,
759
747
  code: str,
@@ -775,7 +763,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
775
763
  associated with this authorization code. Similarly request.scopes
776
764
  must also be set.
777
765
 
778
- Arguments:
766
+ Keyword Arguments:
779
767
 
780
768
  client_id: Unicode client identifier
781
769
  code: Unicode authorization code
@@ -791,20 +779,32 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
791
779
 
792
780
  from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
793
781
 
794
- authorization_code = (
782
+ authorization_code_query = (
795
783
  DBSession.query(static.OAuth2AuthorizationCode)
796
784
  .join(static.OAuth2AuthorizationCode.client)
797
785
  .filter(static.OAuth2AuthorizationCode.code == code)
798
- .filter(static.OAuth2Client.client_id == client_id)
786
+ .filter(static.OAuth2AuthorizationCode.client_id == client.id)
799
787
  .filter(static.OAuth2AuthorizationCode.expire_at > datetime.now())
800
- .one_or_none()
801
788
  )
802
- if authorization_code is not None:
803
- request.user = authorization_code.user
804
- LOG.debug("validate_code => %s", authorization_code is not None)
805
- return authorization_code is not None
789
+ if client.state_required:
790
+ authorization_code_query = authorization_code_query.filter(
791
+ static.OAuth2AuthorizationCode.state == request.state
792
+ )
793
+
794
+ authorization_code = authorization_code_query.one_or_none()
795
+ if authorization_code is None:
796
+ LOG.debug("validate_code => KO, no authorization_code found")
797
+ return False
806
798
 
807
- def validate_grant_type( # pylint: disable=no-self-use,useless-suppression
799
+ if authorization_code.client.pkce_required:
800
+ request.code_challenge = authorization_code.challenge
801
+ request.code_challenge_method = authorization_code.challenge_method
802
+
803
+ request.user = authorization_code.user
804
+ LOG.debug("validate_code => OK")
805
+ return True
806
+
807
+ def validate_grant_type(
808
808
  self,
809
809
  client_id: str,
810
810
  grant_type: str,
@@ -816,7 +816,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
816
816
  """
817
817
  Ensure client is authorized to use the grant_type requested.
818
818
 
819
- Arguments:
819
+ Keyword Arguments:
820
820
 
821
821
  client_id: Unicode client identifier
822
822
  grant_type: Unicode grant type, i.e. authorization_code, password.
@@ -840,7 +840,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
840
840
 
841
841
  return grant_type in ("authorization_code", "refresh_token")
842
842
 
843
- def validate_redirect_uri( # pylint: disable=no-self-use,useless-suppression
843
+ def validate_redirect_uri(
844
844
  self,
845
845
  client_id: str,
846
846
  redirect_uri: str,
@@ -854,7 +854,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
854
854
  All clients should register the absolute URIs of all URIs they intend
855
855
  to redirect to. The registration is outside of the scope of oauthlib.
856
856
 
857
- Arguments:
857
+ Keyword Arguments:
858
858
 
859
859
  client_id: Unicode client identifier
860
860
  redirect_uri: Unicode absolute URI
@@ -879,7 +879,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
879
879
  LOG.debug("validate_redirect_uri %s", client is not None)
880
880
  return client is not None
881
881
 
882
- def validate_refresh_token( # pylint: disable=no-self-use,useless-suppression
882
+ def validate_refresh_token(
883
883
  self,
884
884
  refresh_token: str,
885
885
  client: "c2cgeoportal_commons.models.static.OAuth2Client", # noqa: F821
@@ -893,7 +893,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
893
893
  OBS! The request.user attribute should be set to the resource owner
894
894
  associated with this refresh token.
895
895
 
896
- Arguments:
896
+ Keyword Arguments:
897
897
 
898
898
  refresh_token: Unicode refresh token
899
899
  client: Client object set by you, see authenticate_client.
@@ -922,7 +922,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
922
922
 
923
923
  return bearer_token is not None
924
924
 
925
- def validate_response_type( # pylint: disable=no-self-use,useless-suppression
925
+ def validate_response_type(
926
926
  self,
927
927
  client_id: str,
928
928
  response_type: str,
@@ -934,7 +934,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
934
934
  """
935
935
  Ensure client is authorized to use the response_type requested.
936
936
 
937
- Arguments:
937
+ Keyword Arguments:
938
938
 
939
939
  client_id: Unicode client identifier
940
940
  response_type: Unicode response type, i.e. code, token.
@@ -951,7 +951,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
951
951
 
952
952
  return response_type == "code"
953
953
 
954
- def validate_scopes( # pylint: disable=no-self-use,useless-suppression
954
+ def validate_scopes(
955
955
  self,
956
956
  client_id: str,
957
957
  scopes: List[str],
@@ -963,7 +963,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
963
963
  """
964
964
  Ensure the client is authorized access to requested scopes.
965
965
 
966
- Arguments:
966
+ Keyword Arguments:
967
967
 
968
968
  client_id: Unicode client identifier
969
969
  scopes: List of scopes (defined by you)
@@ -982,7 +982,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
982
982
 
983
983
  return True
984
984
 
985
- def validate_user( # pylint: disable=no-self-use,useless-suppression
985
+ def validate_user(
986
986
  self,
987
987
  username: str,
988
988
  password: str,
@@ -999,7 +999,7 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
999
999
  not set you will be unable to associate a token with a user in the
1000
1000
  persistence method used (commonly, save_bearer_token).
1001
1001
 
1002
- Arguments:
1002
+ Keyword Arguments:
1003
1003
 
1004
1004
  username: Unicode username
1005
1005
  password: Unicode password
@@ -1015,6 +1015,123 @@ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
1015
1015
 
1016
1016
  raise NotImplementedError("Not implemented.")
1017
1017
 
1018
+ def is_pkce_required(self, client_id: int, request: oauthlib.common.Request) -> bool:
1019
+ """
1020
+ Determine if current request requires PKCE.
1021
+
1022
+ Default, False. This is called for both “authorization” and “token” requests.
1023
+
1024
+ Override this method by return True to enable PKCE for everyone. You might want to enable it only
1025
+ for public clients. Note that PKCE can also be used in addition of a client authentication.
1026
+
1027
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to
1028
+ the authorization code interception attack. This specification describes the attack as well as
1029
+ a technique to mitigate against the threat through the use of Proof Key for Code Exchange
1030
+ (PKCE, pronounced “pixy”). See RFC7636.
1031
+
1032
+ Keyword Arguments:
1033
+
1034
+ client_id: Client identifier.
1035
+ request (oauthlib.common.Request): OAuthlib request.
1036
+
1037
+
1038
+ Method is used by:
1039
+
1040
+ Authorization Code Grant
1041
+
1042
+
1043
+ """
1044
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
1045
+
1046
+ client = (
1047
+ DBSession.query(static.OAuth2Client)
1048
+ .filter(static.OAuth2Client.client_id == client_id)
1049
+ .one_or_none()
1050
+ )
1051
+
1052
+ return client and client.pkce_required # type: ignore
1053
+
1054
+ def get_code_challenge(self, code: str, request: oauthlib.common.Request) -> Optional[str]:
1055
+ """
1056
+ Is called for every “token” requests.
1057
+
1058
+ When the server issues the authorization code in the authorization response, it MUST associate the
1059
+ code_challenge and code_challenge_method values with the authorization code so it can be
1060
+ verified later.
1061
+
1062
+ Typically, the code_challenge and code_challenge_method values are stored in encrypted form in
1063
+ the code itself but could alternatively be stored on the server associated with the code.
1064
+ The server MUST NOT include the code_challenge value in client requests in a form that other
1065
+ entities can extract.
1066
+
1067
+ Return the code_challenge associated to the code. If None is returned, code is considered to not
1068
+ be associated to any challenges.
1069
+
1070
+ Keyword Arguments:
1071
+
1072
+ code: Authorization code.
1073
+ request: OAuthlib request.
1074
+
1075
+ Return:
1076
+
1077
+ code_challenge string
1078
+
1079
+ Method is used by:
1080
+
1081
+ Authorization Code Grant - when PKCE is active
1082
+ """
1083
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
1084
+
1085
+ LOG.debug("get_code_challenge")
1086
+
1087
+ authorization_code = (
1088
+ DBSession.query(static.OAuth2AuthorizationCode)
1089
+ .filter(static.OAuth2AuthorizationCode.code == code)
1090
+ .one_or_none()
1091
+ )
1092
+ if authorization_code:
1093
+ return authorization_code.challenge # type: ignore
1094
+ LOG.debug("get_code_challenge authorization_code not found")
1095
+ return None
1096
+
1097
+ def get_code_challenge_method(self, code: str, request: oauthlib.common.Request) -> Optional[str]:
1098
+ """
1099
+ Is called during the “token” request processing.
1100
+
1101
+ When a code_verifier and a code_challenge has been provided.
1102
+
1103
+ See .get_code_challenge.
1104
+
1105
+ Must return plain or S256. You can return a custom value if you have implemented your own
1106
+ AuthorizationCodeGrant class.
1107
+
1108
+ Keyword Arguments:
1109
+
1110
+ code: Authorization code.
1111
+ request: OAuthlib request.
1112
+
1113
+ Return type:
1114
+
1115
+ code_challenge_method string
1116
+
1117
+ Method is used by:
1118
+
1119
+ Authorization Code Grant - when PKCE is active
1120
+ """
1121
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
1122
+
1123
+ LOG.debug("get_code_challenge_method")
1124
+
1125
+ authorization_code = (
1126
+ DBSession.query(static.OAuth2AuthorizationCode)
1127
+ .filter(static.OAuth2AuthorizationCode.code == code)
1128
+ .one_or_none()
1129
+ )
1130
+ if authorization_code:
1131
+ return authorization_code.challenge_method # type: ignore
1132
+ LOG.debug("get_code_challenge_method authorization_code not found")
1133
+ return None
1134
+
1018
1135
 
1019
1136
  def get_oauth_client(settings: Dict[str, Any]) -> oauthlib.oauth2.WebApplicationServer:
1020
1137
  """Get the oauth2 client, with a cache."""