c2cgeoportal-geoportal 2.3.5.80__py3-none-any.whl → 2.9rc1__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 (197) hide show
  1. c2cgeoportal_geoportal/__init__.py +960 -0
  2. c2cgeoportal_geoportal/lib/__init__.py +256 -0
  3. c2cgeoportal_geoportal/lib/authentication.py +250 -0
  4. c2cgeoportal_geoportal/lib/bashcolor.py +46 -0
  5. c2cgeoportal_geoportal/lib/cacheversion.py +75 -0
  6. c2cgeoportal_geoportal/lib/caching.py +176 -0
  7. c2cgeoportal_geoportal/lib/check_collector.py +80 -0
  8. c2cgeoportal_geoportal/lib/checker.py +295 -0
  9. c2cgeoportal_geoportal/lib/common_headers.py +170 -0
  10. c2cgeoportal_geoportal/lib/dbreflection.py +266 -0
  11. c2cgeoportal_geoportal/lib/filter_capabilities.py +360 -0
  12. c2cgeoportal_geoportal/lib/fulltextsearch.py +50 -0
  13. c2cgeoportal_geoportal/lib/functionality.py +166 -0
  14. c2cgeoportal_geoportal/lib/headers.py +62 -0
  15. c2cgeoportal_geoportal/lib/i18n.py +38 -0
  16. c2cgeoportal_geoportal/lib/layers.py +132 -0
  17. c2cgeoportal_geoportal/lib/lingva_extractor.py +937 -0
  18. c2cgeoportal_geoportal/lib/loader.py +57 -0
  19. c2cgeoportal_geoportal/lib/metrics.py +117 -0
  20. c2cgeoportal_geoportal/lib/oauth2.py +1186 -0
  21. c2cgeoportal_geoportal/lib/oidc.py +302 -0
  22. c2cgeoportal_geoportal/lib/wmstparsing.py +353 -0
  23. c2cgeoportal_geoportal/lib/xsd.py +166 -0
  24. c2cgeoportal_geoportal/py.typed +0 -0
  25. c2cgeoportal_geoportal/resources.py +49 -0
  26. c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +26 -0
  27. c2cgeoportal_geoportal/scaffolds/advance_create/cookiecutter.json +18 -0
  28. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.dockerignore +6 -0
  29. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.eslintrc.yaml +19 -0
  30. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.prospector.yaml +30 -0
  31. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +75 -0
  32. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +6 -0
  33. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +58 -0
  34. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.yaml +19 -0
  35. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/development.ini +121 -0
  36. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +139 -0
  37. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +3 -0
  38. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-client.cfg +5 -0
  39. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-server.cfg +6 -0
  40. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/production.ini +38 -0
  41. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/requirements.txt +2 -0
  42. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/setup.py +25 -0
  43. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.api.js +41 -0
  44. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +64 -0
  45. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.commons.js +11 -0
  46. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.config.js +22 -0
  47. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +42 -0
  48. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/authentication.py +10 -0
  49. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/dev.py +14 -0
  50. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/models.py +8 -0
  51. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py +7 -0
  52. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/resources.py +11 -0
  53. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/api/index.js +12 -0
  54. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/js/{{cookiecutter.package}}module.js +25 -0
  55. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/subscribers.py +39 -0
  56. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/views/__init__.py +0 -0
  57. c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +18 -0
  58. c2cgeoportal_geoportal/scaffolds/advance_update/{{cookiecutter.project}}/geoportal/CONST_Makefile +121 -0
  59. c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +18 -0
  60. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore +14 -0
  61. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.editorconfig +17 -0
  62. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +73 -0
  63. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +50 -0
  64. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/update_l10n.yaml +66 -0
  65. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.gitignore +16 -0
  66. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.pre-commit-config.yaml +35 -0
  67. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierignore +1 -0
  68. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierrc.yaml +2 -0
  69. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +75 -0
  70. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +70 -0
  71. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/README.rst +29 -0
  72. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +179 -0
  73. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +22 -0
  74. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/docker-compose-check +25 -0
  75. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -0
  76. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-db.yaml +24 -0
  77. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +511 -0
  78. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-qgis.yaml +21 -0
  79. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.override.sample.yaml +59 -0
  80. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.yaml +121 -0
  81. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +102 -0
  82. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +69 -0
  83. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +430 -0
  84. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/locale/en/LC_MESSAGES/{{cookiecutter.package}}_geoportal-client.po +6 -0
  85. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/desktop.css +0 -0
  86. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/iframe_api.css +0 -0
  87. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/mobile.css +0 -0
  88. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_left.png +0 -0
  89. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_right.png +0 -0
  90. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/blank.png +0 -0
  91. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-blue.png +0 -0
  92. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-gold.png +0 -0
  93. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-green.png +0 -0
  94. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker.png +0 -0
  95. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/robot.txt.tmpl +3 -0
  96. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/Readme.txt +69 -0
  97. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/TM_EUROPE_BORDERS-0.3.sql +70 -0
  98. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/demo.map.tmpl +224 -0
  99. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arial.ttf +0 -0
  100. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbd.ttf +0 -0
  101. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbi.ttf +0 -0
  102. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Ariali.ttf +0 -0
  103. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Bold.ttf +0 -0
  104. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-BoldItalic.ttf +0 -0
  105. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Italic.ttf +0 -0
  106. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Regular.ttf +0 -0
  107. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdana.ttf +0 -0
  108. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanab.ttf +0 -0
  109. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanai.ttf +0 -0
  110. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanaz.ttf +0 -0
  111. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts.conf +12 -0
  112. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.conf +15 -0
  113. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.map.tmpl +87 -0
  114. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/tinyows.xml.tmpl +36 -0
  115. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Landscape.jrxml +207 -0
  116. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Portrait.jrxml +185 -0
  117. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Landscape.jrxml +200 -0
  118. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Portrait.jrxml +170 -0
  119. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/config.yaml.tmpl +175 -0
  120. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/legend.jrxml +109 -0
  121. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation.properties +4 -0
  122. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation_fr.properties +4 -0
  123. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/logo.png +0 -0
  124. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/north.svg +93 -0
  125. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/results.jrxml +25 -0
  126. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +18 -0
  127. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/pyproject.toml +7 -0
  128. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/qgisserver/pg_service.conf.tmpl +15 -0
  129. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/run_alembic.sh +11 -0
  130. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +126 -0
  131. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +132 -0
  132. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/setup.cfg +7 -0
  133. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/spell-ignore-words.txt +5 -0
  134. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/__init__.py +0 -0
  135. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/test_app.py +43 -0
  136. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +195 -0
  137. c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +18 -0
  138. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/.upgrade.yaml +67 -0
  139. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +295 -0
  140. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_create_template/tests/test_testapp.py +48 -0
  141. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +922 -0
  142. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +1503 -0
  143. c2cgeoportal_geoportal/scripts/__init__.py +64 -0
  144. c2cgeoportal_geoportal/scripts/c2cupgrade.py +879 -0
  145. c2cgeoportal_geoportal/scripts/create_demo_theme.py +80 -0
  146. c2cgeoportal_geoportal/scripts/manage_users.py +140 -0
  147. c2cgeoportal_geoportal/scripts/pcreate.py +314 -0
  148. c2cgeoportal_geoportal/scripts/theme2fts.py +347 -0
  149. c2cgeoportal_geoportal/scripts/urllogin.py +81 -0
  150. c2cgeoportal_geoportal/templates/login.html +90 -0
  151. c2cgeoportal_geoportal/templates/notlogin.html +62 -0
  152. c2cgeoportal_geoportal/templates/testi18n.html +12 -0
  153. c2cgeoportal_geoportal/views/__init__.py +59 -0
  154. c2cgeoportal_geoportal/views/dev.py +57 -0
  155. c2cgeoportal_geoportal/views/dynamic.py +208 -0
  156. c2cgeoportal_geoportal/views/entry.py +174 -0
  157. c2cgeoportal_geoportal/views/fulltextsearch.py +189 -0
  158. c2cgeoportal_geoportal/views/geometry_processing.py +75 -0
  159. c2cgeoportal_geoportal/views/i18n.py +129 -0
  160. c2cgeoportal_geoportal/views/layers.py +713 -0
  161. c2cgeoportal_geoportal/views/login.py +679 -0
  162. c2cgeoportal_geoportal/views/mapserverproxy.py +191 -0
  163. c2cgeoportal_geoportal/views/memory.py +90 -0
  164. c2cgeoportal_geoportal/views/ogcproxy.py +120 -0
  165. c2cgeoportal_geoportal/views/pdfreport.py +245 -0
  166. c2cgeoportal_geoportal/views/printproxy.py +143 -0
  167. c2cgeoportal_geoportal/views/profile.py +127 -0
  168. c2cgeoportal_geoportal/views/proxy.py +259 -0
  169. c2cgeoportal_geoportal/views/raster.py +193 -0
  170. c2cgeoportal_geoportal/views/resourceproxy.py +73 -0
  171. c2cgeoportal_geoportal/views/shortener.py +152 -0
  172. c2cgeoportal_geoportal/views/theme.py +1322 -0
  173. c2cgeoportal_geoportal/views/tinyowsproxy.py +189 -0
  174. c2cgeoportal_geoportal/views/vector_tiles.py +83 -0
  175. {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc1.dist-info}/METADATA +21 -24
  176. c2cgeoportal_geoportal-2.9rc1.dist-info/RECORD +192 -0
  177. {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc1.dist-info}/WHEEL +1 -1
  178. c2cgeoportal_geoportal-2.9rc1.dist-info/entry_points.txt +28 -0
  179. c2cgeoportal_geoportal-2.9rc1.dist-info/top_level.txt +2 -0
  180. tests/__init__.py +100 -0
  181. tests/test_cachebuster.py +71 -0
  182. tests/test_caching.py +275 -0
  183. tests/test_checker.py +85 -0
  184. tests/test_decimaljson.py +47 -0
  185. tests/test_headerstween.py +64 -0
  186. tests/test_i18n.py +31 -0
  187. tests/test_init.py +193 -0
  188. tests/test_locale_negociator.py +69 -0
  189. tests/test_mapserverproxy_route_predicate.py +64 -0
  190. tests/test_raster.py +267 -0
  191. tests/test_wmstparsing.py +238 -0
  192. tests/xmlstr.py +103 -0
  193. c2cgeoportal_geoportal-2.3.5.80.dist-info/DESCRIPTION.rst +0 -8
  194. c2cgeoportal_geoportal-2.3.5.80.dist-info/RECORD +0 -7
  195. c2cgeoportal_geoportal-2.3.5.80.dist-info/entry_points.txt +0 -22
  196. c2cgeoportal_geoportal-2.3.5.80.dist-info/metadata.json +0 -1
  197. c2cgeoportal_geoportal-2.3.5.80.dist-info/top_level.txt +0 -1
@@ -0,0 +1,1186 @@
1
+ # Copyright (c) 2021-2024, Camptocamp SA
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ # The views and conclusions contained in the software and documentation are those
25
+ # of the authors and should not be interpreted as representing official policies,
26
+ # either expressed or implied, of the FreeBSD Project.
27
+
28
+ import logging
29
+ from datetime import datetime, timedelta
30
+ from typing import Any, TypedDict
31
+
32
+ import basicauth
33
+ import oauthlib.common
34
+ import oauthlib.oauth2
35
+ import pyramid.threadlocal
36
+ from pyramid.httpexceptions import HTTPBadRequest
37
+
38
+ import c2cgeoportal_commons
39
+ from c2cgeoportal_geoportal.lib.caching import get_region
40
+
41
+ _LOG = logging.getLogger(__name__)
42
+ _OBJECT_CACHE_REGION = get_region("obj")
43
+
44
+
45
+ class _Token(TypedDict):
46
+ access_token: str
47
+ refresh_token: str
48
+ expires_in: int
49
+ state: str | None
50
+
51
+
52
+ class RequestValidator(oauthlib.oauth2.RequestValidator): # type: ignore
53
+ """The oauth2 request validator implementation."""
54
+
55
+ def __init__(self, authorization_expires_in: int) -> None:
56
+ # in minutes
57
+ self.authorization_expires_in = authorization_expires_in
58
+
59
+ def authenticate_client(
60
+ self,
61
+ request: oauthlib.common.Request,
62
+ *args: Any,
63
+ **kwargs: Any,
64
+ ) -> bool:
65
+ """
66
+ Authenticate client through means outside the OAuth 2 spec.
67
+
68
+ Means of authentication is negotiated beforehand and may for example
69
+ be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization
70
+ header.
71
+
72
+ Headers may be accesses through request.headers and parameters found in
73
+ both body and query can be obtained by direct attribute access, i.e.
74
+ request.client_id for client_id in the URL query.
75
+
76
+ Keyword Arguments:
77
+
78
+ request: oauthlib.common.Request
79
+
80
+ Returns: True or False
81
+
82
+ Method is used by:
83
+ - Authorization Code Grant
84
+ - Resource Owner Password Credentials Grant (may be disabled)
85
+ - Client Credentials Grant
86
+ - Refresh Token Grant
87
+
88
+ .. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1
89
+ """
90
+ del args, kwargs
91
+
92
+ _LOG.debug("authenticate_client => unimplemented")
93
+
94
+ raise NotImplementedError("Not implemented, the method `authenticate_client_id` should be used.")
95
+
96
+ def authenticate_client_id(
97
+ self,
98
+ client_id: str,
99
+ request: oauthlib.common.Request,
100
+ *args: Any,
101
+ **kwargs: Any,
102
+ ) -> bool:
103
+ """
104
+ Ensure client_id belong to a non-confidential client.
105
+
106
+ A non-confidential client is one that is not required to authenticate
107
+ through other means, such as using HTTP Basic.
108
+
109
+ Note, while not strictly necessary it can often be very convenient
110
+ to set request.client to the client object associated with the
111
+ given client_id.
112
+
113
+ Method is used by:
114
+ - Authorization Code Grant
115
+ """
116
+ del args, kwargs
117
+
118
+ _LOG.debug("authenticate_client_id %s", client_id)
119
+
120
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
121
+
122
+ assert DBSession is not None
123
+
124
+ params = dict(request.decoded_body)
125
+
126
+ if "client_secret" in params:
127
+ client_secret = params["client_secret"]
128
+ elif "Authorization" in request.headers:
129
+ username, password = basicauth.decode(request.headers["Authorization"])
130
+ assert client_id == username
131
+ client_secret = password
132
+ else:
133
+ # Unable to get the client secret
134
+ return False
135
+
136
+ request.client = (
137
+ DBSession.query(static.OAuth2Client)
138
+ .filter(static.OAuth2Client.client_id == client_id)
139
+ .filter(static.OAuth2Client.secret == client_secret)
140
+ .one_or_none()
141
+ )
142
+
143
+ _LOG.debug("authenticate_client_id => %s", request.client is not None)
144
+ return request.client is not None
145
+
146
+ def client_authentication_required(
147
+ self,
148
+ request: oauthlib.common.Request,
149
+ *args: Any,
150
+ **kwargs: Any,
151
+ ) -> bool:
152
+ """
153
+ Determine if client authentication is required for current request.
154
+
155
+ According to the rfc6749, client authentication is required in the following cases:
156
+ - Resource Owner Password Credentials Grant, when Client type is Confidential or when
157
+ Client was issued client credentials or whenever Client provided client
158
+ authentication, see `Section 4.3.2`_.
159
+ - Authorization Code Grant, when Client type is Confidential or when Client was issued
160
+ client credentials or whenever Client provided client authentication,
161
+ see `Section 4.1.3`_.
162
+ - Refresh Token Grant, when Client type is Confidential or when Client was issued
163
+ client credentials or whenever Client provided client authentication, see
164
+ `Section 6`_
165
+
166
+ Keyword Arguments:
167
+
168
+ request: oauthlib.common.Request
169
+
170
+ Returns: True or False
171
+
172
+ Method is used by:
173
+ - Authorization Code Grant
174
+ - Resource Owner Password Credentials Grant
175
+ - Refresh Token Grant
176
+
177
+ .. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2
178
+ .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
179
+ .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
180
+ """
181
+ del request, args, kwargs
182
+
183
+ _LOG.debug("client_authentication_required => False")
184
+
185
+ return False
186
+
187
+ def confirm_redirect_uri(
188
+ self,
189
+ client_id: str,
190
+ code: str,
191
+ redirect_uri: str,
192
+ client: "c2cgeoportal_commons.models.static.OAuth2Client", # noqa: F821
193
+ *args: Any,
194
+ **kwargs: Any,
195
+ ) -> bool:
196
+ """
197
+ Ensure that the authorization process is correct.
198
+
199
+ Ensure that the authorization process represented by this authorization code began with this
200
+ ``redirect_uri``.
201
+
202
+ If the client specifies a redirect_uri when obtaining code then that
203
+ redirect URI must be bound to the code and verified equal in this
204
+ method, according to RFC 6749 section 4.1.3. Do not compare against
205
+ the client's allowed redirect URIs, but against the URI used when the
206
+ code was saved.
207
+
208
+ Keyword Arguments:
209
+
210
+ client_id: Unicode client identifier
211
+ code: Unicode authorization_code.
212
+ redirect_uri: Unicode absolute URI
213
+ client: Client object set by you, see authenticate_client.
214
+ request: The HTTP Request
215
+
216
+ Returns: True or False
217
+
218
+ Method is used by:
219
+ - Authorization Code Grant (during token request)
220
+ """
221
+ del args, kwargs
222
+
223
+ _LOG.debug("confirm_redirect_uri %s %s", client_id, redirect_uri)
224
+
225
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
226
+
227
+ assert DBSession is not None
228
+
229
+ authorization_code = (
230
+ DBSession.query(static.OAuth2AuthorizationCode)
231
+ .join(static.OAuth2AuthorizationCode.client)
232
+ .filter(static.OAuth2AuthorizationCode.code == code)
233
+ .filter(static.OAuth2Client.client_id == client_id)
234
+ .filter(static.OAuth2AuthorizationCode.redirect_uri == redirect_uri)
235
+ .filter(static.OAuth2AuthorizationCode.expire_at > datetime.now())
236
+ .one_or_none()
237
+ )
238
+ _LOG.debug("confirm_redirect_uri => %s", authorization_code is not None)
239
+ return authorization_code is not None
240
+
241
+ def get_default_redirect_uri(
242
+ self,
243
+ client_id: str,
244
+ request: oauthlib.common.Request,
245
+ *args: Any,
246
+ **kwargs: Any,
247
+ ) -> str:
248
+ """
249
+ Get the default redirect URI for the client.
250
+
251
+ Keyword Arguments:
252
+
253
+ client_id: Unicode client identifier
254
+ request: The HTTP Request
255
+
256
+ Returns: The default redirect URI for the client
257
+
258
+ Method is used by:
259
+ - Authorization Code Grant
260
+ - Implicit Grant
261
+ """
262
+ del request, args, kwargs
263
+
264
+ _LOG.debug("get_default_redirect_uri %s", client_id)
265
+
266
+ raise NotImplementedError("Not implemented.")
267
+
268
+ def get_default_scopes(
269
+ self,
270
+ client_id: str,
271
+ request: oauthlib.common.Request,
272
+ *args: Any,
273
+ **kwargs: Any,
274
+ ) -> list[str]:
275
+ """
276
+ Get the default scopes for the client.
277
+
278
+ Keyword Arguments:
279
+
280
+ client_id: Unicode client identifier
281
+ request: The HTTP Request
282
+
283
+ Returns: List of default scopes
284
+
285
+ Method is used by all core grant types:
286
+ - Authorization Code Grant
287
+ - Implicit Grant
288
+ - Resource Owner Password Credentials Grant
289
+ - Client Credentials grant
290
+ """
291
+ del request, args, kwargs
292
+
293
+ _LOG.debug("get_default_scopes %s", client_id)
294
+
295
+ return ["geomapfish"]
296
+
297
+ def get_original_scopes(
298
+ self,
299
+ refresh_token: str,
300
+ request: oauthlib.common.Request,
301
+ *args: Any,
302
+ **kwargs: Any,
303
+ ) -> list[str]:
304
+ """
305
+ Get the list of scopes associated with the refresh token.
306
+
307
+ Keyword Arguments:
308
+
309
+ refresh_token: Unicode refresh token
310
+ request: The HTTP Request
311
+
312
+ Returns: List of scopes.
313
+
314
+ Method is used by:
315
+ - Refresh token grant
316
+ """
317
+ del refresh_token, request, args, kwargs
318
+
319
+ _LOG.debug("get_original_scopes")
320
+
321
+ return []
322
+
323
+ def introspect_token(
324
+ self,
325
+ token: str,
326
+ token_type_hint: str,
327
+ request: oauthlib.common.Request,
328
+ *args: Any,
329
+ **kwargs: Any,
330
+ ) -> None:
331
+ """
332
+ Introspect an access or refresh token.
333
+
334
+ Called once the introspect request is validated. This method
335
+ should verify the *token* and either return a dictionary with the list of claims associated, or `None`
336
+ in case the token is unknown. Below the list of registered claims you should be interested in:
337
+
338
+ - scope : space-separated list of scopes
339
+ - client_id : client identifier
340
+ - username : human-readable identifier for the resource owner
341
+ - token_type : type of the token
342
+ - exp : integer timestamp indicating when this token will expire
343
+ - iat : integer timestamp indicating when this token was issued
344
+ - nbf : integer timestamp indicating when it can be "not-before" used
345
+ - sub : subject of the token - identifier of the resource owner
346
+ - aud : list of string identifiers representing the intended audience
347
+ - iss : string representing issuer of this token
348
+ - jti : string identifier for the token
349
+ Note that most of them are coming directly from JWT RFC. More details
350
+ can be found in `Introspect Claims`_ or `_JWT Claims`_.
351
+ The implementation can use *token_type_hint* to improve lookup
352
+ efficiency, but must fallback to other types to be compliant with RFC.
353
+ The dict of claims is added to request.token after this method.
354
+
355
+ Keyword Arguments:
356
+
357
+ token: The token string.
358
+ token_type_hint: access_token or refresh_token.
359
+ request: OAuthlib request.
360
+
361
+ Method is used by:
362
+ - Introspect Endpoint (all grants are compatible)
363
+
364
+ .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2
365
+ .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4
366
+ """
367
+ del token, request, args, kwargs
368
+
369
+ _LOG.debug("introspect_token %s", token_type_hint)
370
+
371
+ raise NotImplementedError("Not implemented.")
372
+
373
+ def invalidate_authorization_code(
374
+ self,
375
+ client_id: str,
376
+ code: str,
377
+ request: oauthlib.common.Request,
378
+ *args: Any,
379
+ **kwargs: Any,
380
+ ) -> None:
381
+ """
382
+ Invalidate an authorization code after use.
383
+
384
+ Keyword Arguments:
385
+
386
+ client_id: Unicode client identifier
387
+ code: The authorization code grant (request.code).
388
+ request: The HTTP Request
389
+
390
+ Method is used by:
391
+ - Authorization Code Grant
392
+ """
393
+ del args, kwargs
394
+
395
+ _LOG.debug("invalidate_authorization_code %s", client_id)
396
+
397
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
398
+
399
+ assert DBSession is not None
400
+
401
+ DBSession.delete(
402
+ DBSession.query(static.OAuth2AuthorizationCode)
403
+ .join(static.OAuth2AuthorizationCode.client)
404
+ .filter(static.OAuth2AuthorizationCode.code == code)
405
+ .filter(static.OAuth2Client.client_id == client_id)
406
+ .filter(static.OAuth2AuthorizationCode.user_id == request.user.id)
407
+ .one()
408
+ )
409
+
410
+ def is_within_original_scope(
411
+ self,
412
+ request_scopes: list[str],
413
+ refresh_token: str,
414
+ request: oauthlib.common.Request,
415
+ *args: Any,
416
+ **kwargs: Any,
417
+ ) -> bool:
418
+ """
419
+ Check if requested scopes are within a scope of the refresh token.
420
+
421
+ When access tokens are refreshed the scope of the new token
422
+ needs to be within the scope of the original token. This is
423
+ ensured by checking that all requested scopes strings are on
424
+ the list returned by the get_original_scopes. If this check
425
+ fails, is_within_original_scope is called. The method can be
426
+ used in situations where returning all valid scopes from the
427
+ get_original_scopes is not practical.
428
+
429
+ Keyword Arguments:
430
+
431
+ request_scopes: A list of scopes that were requested by client
432
+ refresh_token: Unicode refresh_token
433
+ request: The HTTP Request
434
+
435
+ Method is used by:
436
+ - Refresh token grant
437
+ """
438
+ del request, args, kwargs
439
+
440
+ _LOG.debug("is_within_original_scope %s %s", request_scopes, refresh_token)
441
+
442
+ return False
443
+
444
+ def revoke_token(
445
+ self,
446
+ token: str,
447
+ token_type_hint: str,
448
+ request: oauthlib.common.Request,
449
+ *args: Any,
450
+ **kwargs: Any,
451
+ ) -> None:
452
+ """
453
+ Revoke an access or refresh token.
454
+
455
+ Keyword Arguments:
456
+
457
+ token: The token string.
458
+ token_type_hint: access_token or refresh_token.
459
+ request: The HTTP Request
460
+
461
+ Method is used by:
462
+ - Revocation Endpoint
463
+ """
464
+ del token, request, args, kwargs
465
+
466
+ _LOG.debug("revoke_token %s", token_type_hint)
467
+
468
+ raise NotImplementedError("Not implemented.")
469
+
470
+ def rotate_refresh_token(self, request: oauthlib.common.Request) -> bool:
471
+ """
472
+ Determine whether to rotate the refresh token. Default, yes.
473
+
474
+ When access tokens are refreshed the old refresh token can be kept
475
+ or replaced with a new one (rotated). Return True to rotate and
476
+ and False for keeping original.
477
+
478
+ Keyword Arguments:
479
+
480
+ request: oauthlib.common.Request
481
+
482
+ Method is used by:
483
+ - Refresh Token Grant
484
+ """
485
+ del request
486
+
487
+ _LOG.debug("rotate_refresh_token")
488
+
489
+ return True
490
+
491
+ def save_authorization_code(
492
+ self,
493
+ client_id: str,
494
+ code: dict[str, str],
495
+ request: oauthlib.common.Request,
496
+ *args: Any,
497
+ **kwargs: Any,
498
+ ) -> None:
499
+ """
500
+ Persist the authorization_code.
501
+
502
+ The code should at minimum be stored with:
503
+ - the client_id (client_id)
504
+ - the redirect URI used (request.redirect_uri)
505
+ - a resource owner / user (request.user)
506
+ - the authorized scopes (request.scopes)
507
+ - the client state, if given (code.get('state'))
508
+
509
+ The 'code' argument is actually a dictionary, containing at least a
510
+ 'code' key with the actual authorization code:
511
+
512
+ {'code': '<secret>'}
513
+
514
+ It may also have a 'state' key containing a nonce for the client, if it
515
+ chose to send one. That value should be saved and used in
516
+ 'validate_code'.
517
+
518
+ Keyword Arguments:
519
+
520
+ client_id: Unicode client identifier
521
+ code: A dict of the authorization code grant and, optionally, state.
522
+ request: The HTTP Request
523
+
524
+ Method is used by:
525
+ - Authorization Code Grant
526
+
527
+ To support PKCE, you MUST associate the code with:
528
+
529
+ Code Challenge (request.code_challenge) and
530
+ Code Challenge Method (request.code_challenge_method)
531
+ """
532
+ del args, kwargs
533
+
534
+ _LOG.debug("save_authorization_code %s", client_id)
535
+
536
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
537
+
538
+ assert DBSession is not None
539
+
540
+ user = pyramid.threadlocal.get_current_request().user
541
+
542
+ # Don't allows to have two authentications for the same user and the same client
543
+ authorization_code = (
544
+ DBSession.query(static.OAuth2AuthorizationCode)
545
+ .filter(static.OAuth2AuthorizationCode.client_id == request.client.id)
546
+ .filter(static.OAuth2AuthorizationCode.user_id == user.id)
547
+ .one_or_none()
548
+ )
549
+
550
+ if authorization_code is not None:
551
+ authorization_code.expire_at = datetime.now() + timedelta(minutes=self.authorization_expires_in)
552
+ else:
553
+ authorization_code = static.OAuth2AuthorizationCode()
554
+ authorization_code.client_id = request.client.id
555
+ authorization_code.user_id = user.id
556
+ authorization_code.expire_at = datetime.now() + timedelta(minutes=self.authorization_expires_in)
557
+ authorization_code.state = code.get("state")
558
+
559
+ authorization_code.code = code["code"]
560
+ authorization_code.redirect_uri = request.redirect_uri
561
+
562
+ client = (
563
+ DBSession.query(static.OAuth2Client)
564
+ .filter(static.OAuth2Client.client_id == client_id)
565
+ .one_or_none()
566
+ )
567
+ if client and client.state_required and not code.get("state"):
568
+ raise HTTPBadRequest("Client is missing the state parameter.")
569
+
570
+ if client and client.pkce_required:
571
+ authorization_code.challenge = request.code_challenge
572
+ authorization_code.challenge_method = request.code_challenge_method
573
+
574
+ DBSession.add(authorization_code)
575
+
576
+ def save_bearer_token(
577
+ self,
578
+ token: _Token,
579
+ request: oauthlib.common.Request,
580
+ *args: Any,
581
+ **kwargs: Any,
582
+ ) -> None:
583
+ """
584
+ Persist the Bearer token.
585
+
586
+ The Bearer token should at minimum be associated with:
587
+ - a client and it's client_id, if available
588
+ - a resource owner / user (request.user)
589
+ - authorized scopes (request.scopes)
590
+ - an expiration time
591
+ - a refresh token, if issued
592
+
593
+ The Bearer token dict may hold a number of items::
594
+
595
+ {
596
+ 'token_type': 'Bearer',
597
+ 'access_token': '<secret>',
598
+ 'expires_in': 3600,
599
+ 'scope': 'string of space separated authorized scopes',
600
+ 'refresh_token': '<secret>', # if issued
601
+ 'state': 'given_by_client', # if supplied by client
602
+ }
603
+
604
+ Note that while "scope" is a string-separated list of authorized scopes,
605
+ the original list is still available in request.scopes
606
+
607
+ Keyword Arguments:
608
+
609
+ client_id: Unicode client identifier
610
+ token: A Bearer token dict
611
+ request: The HTTP Request
612
+
613
+ Returns: The default redirect URI for the client
614
+
615
+ Method is used by all core grant types issuing Bearer tokens:
616
+ - Authorization Code Grant
617
+ - Implicit Grant
618
+ - Resource Owner Password Credentials Grant (might not associate a client)
619
+ - Client Credentials grant
620
+ """
621
+ del args, kwargs
622
+
623
+ _LOG.debug("save_bearer_token")
624
+
625
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
626
+
627
+ assert DBSession is not None
628
+
629
+ # Don't allows to have tow token for one user end one client
630
+ bearer_token = (
631
+ DBSession.query(static.OAuth2BearerToken)
632
+ .filter(static.OAuth2BearerToken.client_id == request.client.id)
633
+ .filter(static.OAuth2BearerToken.user_id == request.user.id)
634
+ .one_or_none()
635
+ )
636
+
637
+ if bearer_token is None:
638
+ bearer_token = static.OAuth2BearerToken()
639
+ bearer_token.client_id = request.client.id
640
+ bearer_token.user_id = request.user.id
641
+ DBSession.add(bearer_token)
642
+
643
+ bearer_token.access_token = token["access_token"]
644
+ bearer_token.refresh_token = token["refresh_token"]
645
+ bearer_token.expire_at = datetime.now() + timedelta(seconds=float(token["expires_in"]))
646
+ bearer_token.state = token.get("state")
647
+
648
+ def validate_bearer_token(
649
+ self,
650
+ token: str,
651
+ scopes: list[str],
652
+ request: oauthlib.common.Request,
653
+ ) -> bool:
654
+ """
655
+ Ensure the Bearer token is valid and authorized access to scopes.
656
+
657
+ Keyword Arguments:
658
+
659
+ token: A string of random characters.
660
+ scopes: A list of scopes associated with the protected resource.
661
+ request: The HTTP Request
662
+
663
+ A key to OAuth 2 security and restricting impact of leaked tokens is
664
+ the short expiration time of tokens, *always ensure the token has not
665
+ expired!*.
666
+
667
+ Two different approaches to scope validation:
668
+
669
+ 1) all(scopes). The token must be authorized access to all scopes
670
+ associated with the resource. For example, the
671
+ token has access to ``read-only`` and ``images``,
672
+ thus the client can view images but not upload new.
673
+ Allows for fine grained access control through
674
+ combining various scopes.
675
+
676
+ 2) any(scopes). The token must be authorized access to one of the
677
+ scopes associated with the resource. For example,
678
+ token has access to ``read-only-images``.
679
+ Allows for fine grained, although arguably less
680
+ convenient, access control.
681
+
682
+ A powerful way to use scopes would mimic UNIX ACLs and see a scope
683
+ as a group with certain privileges. For a restful API these might
684
+ map to HTTP verbs instead of read, write and execute.
685
+
686
+ Note, the request.user attribute can be set to the resource owner
687
+ associated with this token. Similarly the request.client and
688
+ request.scopes attribute can be set to associated client object
689
+ and authorized scopes. If you then use a decorator such as the
690
+ one provided for django these attributes will be made available
691
+ in all protected views as keyword arguments.
692
+
693
+ Keyword Arguments:
694
+
695
+ token: Unicode Bearer token
696
+ scopes: List of scopes (defined by you)
697
+ request: The HTTP Request
698
+
699
+ Method is indirectly used by all core Bearer token issuing grant types:
700
+ - Authorization Code Grant
701
+ - Implicit Grant
702
+ - Resource Owner Password Credentials Grant
703
+ - Client Credentials Grant
704
+ """
705
+
706
+ _LOG.debug("validate_bearer_token %s", scopes)
707
+
708
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
709
+
710
+ assert DBSession is not None
711
+
712
+ bearer_token = (
713
+ DBSession.query(static.OAuth2BearerToken)
714
+ .join(static.User)
715
+ .filter(static.OAuth2BearerToken.access_token == token)
716
+ .filter(static.OAuth2BearerToken.expire_at > datetime.now())
717
+ ).one_or_none()
718
+
719
+ if bearer_token is not None:
720
+ request.user = bearer_token.user
721
+
722
+ _LOG.debug("validate_bearer_token => %s", bearer_token is not None)
723
+ return bearer_token is not None
724
+
725
+ def validate_client_id(
726
+ self,
727
+ client_id: str,
728
+ request: oauthlib.common.Request,
729
+ *args: Any,
730
+ **kwargs: Any,
731
+ ) -> bool:
732
+ """
733
+ Ensure client_id belong to a valid and active client.
734
+
735
+ Note, while not strictly necessary it can often be very convenient
736
+ to set request.client to the client object associated with the
737
+ given client_id.
738
+
739
+ Keyword Arguments:
740
+
741
+ client_id: Unicode client identifier
742
+ request: oauthlib.common.Request
743
+
744
+ Method is used by:
745
+ - Authorization Code Grant
746
+ - Implicit Grant
747
+ """
748
+ del args, kwargs
749
+
750
+ _LOG.debug("validate_client_id")
751
+
752
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
753
+
754
+ assert DBSession is not None
755
+
756
+ client = (
757
+ DBSession.query(static.OAuth2Client)
758
+ .filter(static.OAuth2Client.client_id == client_id)
759
+ .one_or_none()
760
+ )
761
+ if client is not None:
762
+ request.client = client
763
+ return client is not None
764
+
765
+ def validate_code(
766
+ self,
767
+ client_id: str,
768
+ code: str,
769
+ client: "c2cgeoportal_commons.models.static.OAuth2Client", # noqa: F821
770
+ request: oauthlib.common.Request,
771
+ *args: Any,
772
+ **kwargs: Any,
773
+ ) -> bool:
774
+ """
775
+ Verify that the authorization_code is valid and assigned to the given client.
776
+
777
+ Before returning true, set the following based on the information stored
778
+ with the code in 'save_authorization_code':
779
+
780
+ - request.user
781
+ - request.state (if given)
782
+ - request.scopes
783
+ OBS! The request.user attribute should be set to the resource owner
784
+ associated with this authorization code. Similarly request.scopes
785
+ must also be set.
786
+
787
+ Keyword Arguments:
788
+
789
+ client_id: Unicode client identifier
790
+ code: Unicode authorization code
791
+ client: Client object set by you, see authenticate_client.
792
+ request: The HTTP Request
793
+
794
+ Method is used by:
795
+ - Authorization Code Grant
796
+ """
797
+ del args, kwargs
798
+
799
+ _LOG.debug("validate_code %s", client_id)
800
+
801
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
802
+
803
+ assert DBSession is not None
804
+
805
+ authorization_code_query = (
806
+ DBSession.query(static.OAuth2AuthorizationCode)
807
+ .join(static.OAuth2AuthorizationCode.client)
808
+ .filter(static.OAuth2AuthorizationCode.code == code)
809
+ .filter(static.OAuth2AuthorizationCode.client_id == client.id)
810
+ .filter(static.OAuth2AuthorizationCode.expire_at > datetime.now())
811
+ )
812
+ if client.state_required:
813
+ authorization_code_query = authorization_code_query.filter(
814
+ static.OAuth2AuthorizationCode.state == request.state
815
+ )
816
+
817
+ authorization_code = authorization_code_query.one_or_none()
818
+ if authorization_code is None:
819
+ _LOG.debug("validate_code => KO, no authorization_code found")
820
+ return False
821
+
822
+ if authorization_code.client.pkce_required:
823
+ request.code_challenge = authorization_code.challenge
824
+ request.code_challenge_method = authorization_code.challenge_method
825
+
826
+ request.user = authorization_code.user
827
+ _LOG.debug("validate_code => OK")
828
+ return True
829
+
830
+ def validate_grant_type(
831
+ self,
832
+ client_id: str,
833
+ grant_type: str,
834
+ client: "c2cgeoportal_commons.models.static.OAuth2Client",
835
+ request: oauthlib.common.Request,
836
+ *args: Any,
837
+ **kwargs: Any,
838
+ ) -> bool:
839
+ """
840
+ Ensure client is authorized to use the grant_type requested.
841
+
842
+ Keyword Arguments:
843
+
844
+ client_id: Unicode client identifier
845
+ grant_type: Unicode grant type, i.e. authorization_code, password.
846
+ client: Client object set by you, see authenticate_client.
847
+ request: The HTTP Request
848
+
849
+ Method is used by:
850
+ - Authorization Code Grant
851
+ - Resource Owner Password Credentials Grant
852
+ - Client Credentials Grant
853
+ - Refresh Token Grant
854
+ """
855
+ del client, request, args, kwargs
856
+
857
+ _LOG.debug(
858
+ "validate_grant_type %s %s => %s",
859
+ client_id,
860
+ grant_type,
861
+ grant_type in ("authorization_code", "refresh_token"),
862
+ )
863
+
864
+ return grant_type in ("authorization_code", "refresh_token")
865
+
866
+ def validate_redirect_uri(
867
+ self,
868
+ client_id: str,
869
+ redirect_uri: str,
870
+ request: oauthlib.common.Request,
871
+ *args: Any,
872
+ **kwargs: Any,
873
+ ) -> bool:
874
+ """
875
+ Ensure client is authorized to redirect to the redirect_uri requested.
876
+
877
+ All clients should register the absolute URIs of all URIs they intend
878
+ to redirect to. The registration is outside of the scope of oauthlib.
879
+
880
+ Keyword Arguments:
881
+
882
+ client_id: Unicode client identifier
883
+ redirect_uri: Unicode absolute URI
884
+ request: The HTTP Request
885
+
886
+ Method is used by:
887
+ - Authorization Code Grant
888
+ - Implicit Grant
889
+ """
890
+ del request, args, kwargs
891
+
892
+ _LOG.debug("validate_redirect_uri %s %s", client_id, redirect_uri)
893
+
894
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
895
+
896
+ assert DBSession is not None
897
+
898
+ client = (
899
+ DBSession.query(static.OAuth2Client)
900
+ .filter(static.OAuth2Client.client_id == client_id)
901
+ .filter(static.OAuth2Client.redirect_uri == redirect_uri)
902
+ .one_or_none()
903
+ )
904
+ _LOG.debug("validate_redirect_uri %s", client is not None)
905
+ return client is not None
906
+
907
+ def validate_refresh_token(
908
+ self,
909
+ refresh_token: str,
910
+ client: "c2cgeoportal_commons.models.static.OAuth2Client", # noqa: F821
911
+ request: oauthlib.common.Request,
912
+ *args: Any,
913
+ **kwargs: Any,
914
+ ) -> bool:
915
+ """
916
+ Ensure the Bearer token is valid and authorized access to scopes.
917
+
918
+ OBS! The request.user attribute should be set to the resource owner
919
+ associated with this refresh token.
920
+
921
+ Keyword Arguments:
922
+
923
+ refresh_token: Unicode refresh token
924
+ client: Client object set by you, see authenticate_client.
925
+ request: The HTTP Request
926
+
927
+ Method is used by:
928
+ - Authorization Code Grant (indirectly by issuing refresh tokens)
929
+ - Resource Owner Password Credentials Grant (also indirectly)
930
+ - Refresh Token Grant
931
+ """
932
+ del args, kwargs
933
+
934
+ _LOG.debug("validate_refresh_token %s", client.client_id if client else None)
935
+
936
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
937
+
938
+ assert DBSession is not None
939
+
940
+ bearer_token = (
941
+ DBSession.query(static.OAuth2BearerToken)
942
+ .filter(static.OAuth2BearerToken.refresh_token == refresh_token)
943
+ .filter(static.OAuth2BearerToken.client_id == request.client.id)
944
+ .one_or_none()
945
+ )
946
+
947
+ if bearer_token is not None:
948
+ request.user = bearer_token.user
949
+
950
+ return bearer_token is not None
951
+
952
+ def validate_response_type(
953
+ self,
954
+ client_id: str,
955
+ response_type: str,
956
+ client: "c2cgeoportal_commons.models.static.OAuth2Client",
957
+ request: oauthlib.common.Request,
958
+ *args: Any,
959
+ **kwargs: Any,
960
+ ) -> bool:
961
+ """
962
+ Ensure client is authorized to use the response_type requested.
963
+
964
+ Keyword Arguments:
965
+
966
+ client_id: Unicode client identifier
967
+ response_type: Unicode response type, i.e. code, token.
968
+ client: Client object set by you, see authenticate_client.
969
+ request: The HTTP Request
970
+
971
+ Method is used by:
972
+ - Authorization Code Grant
973
+ - Implicit Grant
974
+ """
975
+ del client, request, args, kwargs
976
+
977
+ _LOG.debug("validate_response_type %s %s", client_id, response_type)
978
+
979
+ return response_type == "code"
980
+
981
+ def validate_scopes(
982
+ self,
983
+ client_id: str,
984
+ scopes: list[str],
985
+ client: "c2cgeoportal_commons.models.static.OAuth2Client",
986
+ request: oauthlib.common.Request,
987
+ *args: Any,
988
+ **kwargs: Any,
989
+ ) -> bool:
990
+ """
991
+ Ensure the client is authorized access to requested scopes.
992
+
993
+ Keyword Arguments:
994
+
995
+ client_id: Unicode client identifier
996
+ scopes: List of scopes (defined by you)
997
+ client: Client object set by you, see authenticate_client.
998
+ request: The HTTP Request
999
+
1000
+ Method is used by all core grant types:
1001
+ - Authorization Code Grant
1002
+ - Implicit Grant
1003
+ - Resource Owner Password Credentials Grant
1004
+ - Client Credentials Grant
1005
+ """
1006
+ del client, request, args, kwargs
1007
+
1008
+ _LOG.debug("validate_scopes %s %s", client_id, scopes)
1009
+
1010
+ return True
1011
+
1012
+ def validate_user(
1013
+ self,
1014
+ username: str,
1015
+ password: str,
1016
+ client: "c2cgeoportal_commons.models.static.OAuth2Client", # noqa: F821
1017
+ request: oauthlib.common.Request,
1018
+ *args: Any,
1019
+ **kwargs: Any,
1020
+ ) -> bool:
1021
+ """
1022
+ Ensure the username and password is valid.
1023
+
1024
+ OBS! The validation should also set the user attribute of the request
1025
+ to a valid resource owner, i.e. request.user = username or similar. If
1026
+ not set you will be unable to associate a token with a user in the
1027
+ persistence method used (commonly, save_bearer_token).
1028
+
1029
+ Keyword Arguments:
1030
+
1031
+ username: Unicode username
1032
+ password: Unicode password
1033
+ client: Client object set by you, see authenticate_client.
1034
+ request: The HTTP Request
1035
+
1036
+ Method is used by:
1037
+ - Resource Owner Password Credentials Grant
1038
+ """
1039
+ del password, client, request, args, kwargs
1040
+
1041
+ _LOG.debug("validate_user %s", username)
1042
+
1043
+ raise NotImplementedError("Not implemented.")
1044
+
1045
+ def is_pkce_required(self, client_id: int, request: oauthlib.common.Request) -> bool:
1046
+ """
1047
+ Determine if current request requires PKCE.
1048
+
1049
+ Default, False. This is called for both “authorization” and “token” requests.
1050
+
1051
+ Override this method by return True to enable PKCE for everyone. You might want to enable it only
1052
+ for public clients. Note that PKCE can also be used in addition of a client authentication.
1053
+
1054
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to
1055
+ the authorization code interception attack. This specification describes the attack as well as
1056
+ a technique to mitigate against the threat through the use of Proof Key for Code Exchange
1057
+ (PKCE, pronounced “pixy”). See RFC7636.
1058
+
1059
+ Keyword Arguments:
1060
+
1061
+ client_id: Client identifier.
1062
+ request (oauthlib.common.Request): OAuthlib request.
1063
+
1064
+
1065
+ Method is used by:
1066
+
1067
+ Authorization Code Grant
1068
+
1069
+
1070
+ """
1071
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
1072
+
1073
+ assert DBSession is not None
1074
+
1075
+ client = (
1076
+ DBSession.query(static.OAuth2Client)
1077
+ .filter(static.OAuth2Client.client_id == client_id)
1078
+ .one_or_none()
1079
+ )
1080
+
1081
+ return client and client.pkce_required # type: ignore
1082
+
1083
+ def get_code_challenge(self, code: str, request: oauthlib.common.Request) -> str | None:
1084
+ """
1085
+ Is called for every “token” requests.
1086
+
1087
+ When the server issues the authorization code in the authorization response, it MUST associate the
1088
+ code_challenge and code_challenge_method values with the authorization code so it can be
1089
+ verified later.
1090
+
1091
+ Typically, the code_challenge and code_challenge_method values are stored in encrypted form in
1092
+ the code itself but could alternatively be stored on the server associated with the code.
1093
+ The server MUST NOT include the code_challenge value in client requests in a form that other
1094
+ entities can extract.
1095
+
1096
+ Return the code_challenge associated to the code. If None is returned, code is considered to not
1097
+ be associated to any challenges.
1098
+
1099
+ Keyword Arguments:
1100
+
1101
+ code: Authorization code.
1102
+ request: OAuthlib request.
1103
+
1104
+ Return:
1105
+
1106
+ code_challenge string
1107
+
1108
+ Method is used by:
1109
+
1110
+ Authorization Code Grant - when PKCE is active
1111
+ """
1112
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
1113
+
1114
+ assert DBSession is not None
1115
+
1116
+ _LOG.debug("get_code_challenge")
1117
+
1118
+ authorization_code = (
1119
+ DBSession.query(static.OAuth2AuthorizationCode)
1120
+ .filter(static.OAuth2AuthorizationCode.code == code)
1121
+ .one_or_none()
1122
+ )
1123
+ if authorization_code:
1124
+ return authorization_code.challenge
1125
+ _LOG.debug("get_code_challenge authorization_code not found")
1126
+ return None
1127
+
1128
+ def get_code_challenge_method(self, code: str, request: oauthlib.common.Request) -> str | None:
1129
+ """
1130
+ Is called during the “token” request processing.
1131
+
1132
+ When a code_verifier and a code_challenge has been provided.
1133
+
1134
+ See .get_code_challenge.
1135
+
1136
+ Must return plain or S256. You can return a custom value if you have implemented your own
1137
+ AuthorizationCodeGrant class.
1138
+
1139
+ Keyword Arguments:
1140
+
1141
+ code: Authorization code.
1142
+ request: OAuthlib request.
1143
+
1144
+ Return type:
1145
+
1146
+ code_challenge_method string
1147
+
1148
+ Method is used by:
1149
+
1150
+ Authorization Code Grant - when PKCE is active
1151
+ """
1152
+ from c2cgeoportal_commons.models import DBSession, static # pylint: disable=import-outside-toplevel
1153
+
1154
+ assert DBSession is not None
1155
+
1156
+ _LOG.debug("get_code_challenge_method")
1157
+
1158
+ authorization_code = (
1159
+ DBSession.query(static.OAuth2AuthorizationCode)
1160
+ .filter(static.OAuth2AuthorizationCode.code == code)
1161
+ .one_or_none()
1162
+ )
1163
+ if authorization_code:
1164
+ return authorization_code.challenge_method
1165
+ _LOG.debug("get_code_challenge_method authorization_code not found")
1166
+ return None
1167
+
1168
+
1169
+ def get_oauth_client(settings: dict[str, Any]) -> oauthlib.oauth2.WebApplicationServer:
1170
+ """Get the oauth2 client, with a cache."""
1171
+ authentication_settings = settings.get("authentication", {})
1172
+ return _get_oauth_client_cache(
1173
+ authentication_settings.get("oauth2_authorization_expire_minutes", 10),
1174
+ authentication_settings.get("oauth2_token_expire_minutes", 60),
1175
+ )
1176
+
1177
+
1178
+ @_OBJECT_CACHE_REGION.cache_on_arguments()
1179
+ def _get_oauth_client_cache(
1180
+ authorization_expire_minutes: int, token_expire_minutes: int
1181
+ ) -> oauthlib.oauth2.WebApplicationServer:
1182
+ """Get the oauth2 client, with a cache."""
1183
+ return oauthlib.oauth2.WebApplicationServer(
1184
+ RequestValidator(authorization_expires_in=authorization_expire_minutes),
1185
+ token_expires_in=token_expire_minutes * 60,
1186
+ )