c2cgeoportal-geoportal 2.9rc83__py3-none-any.whl → 2.9.0.352__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 (93) hide show
  1. c2cgeoportal_geoportal/__init__.py +28 -8
  2. c2cgeoportal_geoportal/lib/__init__.py +1 -1
  3. c2cgeoportal_geoportal/lib/authentication.py +4 -1
  4. c2cgeoportal_geoportal/lib/bashcolor.py +1 -1
  5. c2cgeoportal_geoportal/lib/cacheversion.py +1 -1
  6. c2cgeoportal_geoportal/lib/caching.py +1 -1
  7. c2cgeoportal_geoportal/lib/check_collector.py +1 -1
  8. c2cgeoportal_geoportal/lib/checker.py +1 -1
  9. c2cgeoportal_geoportal/lib/dbreflection.py +1 -1
  10. c2cgeoportal_geoportal/lib/filter_capabilities.py +1 -1
  11. c2cgeoportal_geoportal/lib/fulltextsearch.py +1 -1
  12. c2cgeoportal_geoportal/lib/functionality.py +1 -1
  13. c2cgeoportal_geoportal/lib/headers.py +1 -1
  14. c2cgeoportal_geoportal/lib/i18n.py +1 -1
  15. c2cgeoportal_geoportal/lib/layers.py +1 -1
  16. c2cgeoportal_geoportal/lib/loader.py +1 -1
  17. c2cgeoportal_geoportal/lib/metrics.py +1 -1
  18. c2cgeoportal_geoportal/lib/oauth2.py +1 -1
  19. c2cgeoportal_geoportal/lib/oidc.py +109 -71
  20. c2cgeoportal_geoportal/lib/wmstparsing.py +1 -1
  21. c2cgeoportal_geoportal/lib/xsd.py +1 -1
  22. c2cgeoportal_geoportal/resources.py +1 -1
  23. c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +1 -12
  24. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +3 -3
  25. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +1 -1
  26. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +2 -2
  27. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +1 -1
  28. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +1 -0
  29. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +11 -1
  30. c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +2 -0
  31. c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +2 -0
  32. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +1 -1
  33. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +1 -7
  34. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +1 -1
  35. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +4 -4
  36. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +2 -0
  37. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +1 -8
  38. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -2
  39. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +8 -4
  40. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +3 -0
  41. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +1 -5
  42. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +2 -2
  43. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +2 -0
  44. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +1 -1
  45. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +1 -1
  46. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +6 -4
  47. c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +2 -0
  48. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +14 -6
  49. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +2 -0
  50. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +16 -8
  51. c2cgeoportal_geoportal/scripts/__init__.py +1 -1
  52. c2cgeoportal_geoportal/scripts/c2cupgrade.py +2 -2
  53. c2cgeoportal_geoportal/scripts/create_demo_theme.py +1 -1
  54. c2cgeoportal_geoportal/scripts/manage_users.py +1 -1
  55. c2cgeoportal_geoportal/scripts/pcreate.py +11 -5
  56. c2cgeoportal_geoportal/scripts/theme2fts.py +141 -36
  57. c2cgeoportal_geoportal/scripts/urllogin.py +1 -1
  58. c2cgeoportal_geoportal/views/__init__.py +1 -1
  59. c2cgeoportal_geoportal/views/dev.py +1 -1
  60. c2cgeoportal_geoportal/views/entry.py +4 -2
  61. c2cgeoportal_geoportal/views/fulltextsearch.py +10 -4
  62. c2cgeoportal_geoportal/views/geometry_processing.py +1 -1
  63. c2cgeoportal_geoportal/views/i18n.py +1 -1
  64. c2cgeoportal_geoportal/views/layers.py +1 -1
  65. c2cgeoportal_geoportal/views/login.py +18 -8
  66. c2cgeoportal_geoportal/views/mapserverproxy.py +1 -1
  67. c2cgeoportal_geoportal/views/memory.py +1 -1
  68. c2cgeoportal_geoportal/views/ogcproxy.py +1 -1
  69. c2cgeoportal_geoportal/views/pdfreport.py +1 -1
  70. c2cgeoportal_geoportal/views/printproxy.py +1 -1
  71. c2cgeoportal_geoportal/views/profile.py +1 -1
  72. c2cgeoportal_geoportal/views/raster.py +1 -1
  73. c2cgeoportal_geoportal/views/resourceproxy.py +1 -1
  74. c2cgeoportal_geoportal/views/shortener.py +23 -7
  75. c2cgeoportal_geoportal/views/theme.py +18 -4
  76. c2cgeoportal_geoportal/views/tinyowsproxy.py +12 -6
  77. c2cgeoportal_geoportal/views/vector_tiles.py +1 -1
  78. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/METADATA +7 -1
  79. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/RECORD +93 -93
  80. tests/__init__.py +1 -1
  81. tests/test_cachebuster.py +1 -1
  82. tests/test_checker.py +1 -1
  83. tests/test_decimaljson.py +1 -1
  84. tests/test_headerstween.py +1 -1
  85. tests/test_init.py +1 -1
  86. tests/test_locale_negociator.py +1 -1
  87. tests/test_mapserverproxy_route_predicate.py +1 -1
  88. tests/test_raster.py +1 -1
  89. tests/test_wmstparsing.py +1 -1
  90. tests/xmlstr.py +1 -1
  91. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/WHEEL +0 -0
  92. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/entry_points.txt +0 -0
  93. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -42,10 +42,8 @@ import c2cwsgiutils.db
42
42
  import c2cwsgiutils.index
43
43
  import dateutil.parser
44
44
  import pyramid.config
45
- import pyramid.renderers
46
45
  import pyramid.request
47
46
  import pyramid.response
48
- import pyramid.security
49
47
  import sqlalchemy
50
48
  import sqlalchemy.orm
51
49
  import zope.event.classhandler
@@ -408,17 +406,36 @@ def create_get_user_from_request(
408
406
  access_token_expires = dateutil.parser.isoparse(
409
407
  user_info_remember["access_token_expires"]
410
408
  )
411
- if access_token_expires < datetime.datetime.now():
409
+ # If access_token_expires is not offset-aware make it offset-aware
410
+ if access_token_expires.tzinfo is None:
411
+ _LOG.warning(
412
+ "access_token_expires is not offset-aware, "
413
+ "make it offset-aware by replacing tzinfo with UTC"
414
+ )
415
+ access_token_expires = access_token_expires.replace(tzinfo=datetime.timezone.utc)
416
+ if access_token_expires < datetime.datetime.now(datetime.timezone.utc):
412
417
  if user_info_remember["refresh_token_expires"] is None:
413
418
  return None
419
+ refresh_token = request.cookies.get("refresh_token")
420
+ if refresh_token is None:
421
+ return None
414
422
  refresh_token_expires = dateutil.parser.isoparse(
415
423
  user_info_remember["refresh_token_expires"]
416
424
  )
417
- if refresh_token_expires < datetime.datetime.now():
425
+ # If refresh_token_expires is not offset-aware make it offset-aware
426
+ if refresh_token_expires.tzinfo is None:
427
+ _LOG.warning(
428
+ "refresh_token_expires is not offset-aware, "
429
+ "make it offset-aware by replacing tzinfo with UTC"
430
+ )
431
+ refresh_token_expires = refresh_token_expires.replace(
432
+ tzinfo=datetime.timezone.utc
433
+ )
434
+ if refresh_token_expires < datetime.datetime.now(datetime.timezone.utc):
418
435
  return None
419
436
  token_response = oidc.get_oidc_client(
420
437
  request, request.host
421
- ).exchange_refresh_token(user_info_remember["refresh_token"])
438
+ ).exchange_refresh_token(refresh_token)
422
439
  user_info_remember = oidc.OidcRemember(request).remember(
423
440
  token_response, request.host
424
441
  )
@@ -430,7 +447,7 @@ def create_get_user_from_request(
430
447
  request.user_ = (
431
448
  DBSession.query(User)
432
449
  .filter_by(username=username, deactivated=False)
433
- .options(joinedload(User.roles))
450
+ .options(joinedload(User.roles), joinedload(User.settings_role))
434
451
  .first()
435
452
  )
436
453
 
@@ -674,9 +691,11 @@ def includeme(config: pyramid.config.Configurator) -> None:
674
691
  "/mapserv_proxy/{ogcserver}/wfs3/*path",
675
692
  mapserverproxy=True,
676
693
  pregenerator=C2CPregenerator(role=True),
677
- request_method="GET",
694
+ request_method=("GET", "POST", "PUT", "DELETE", "PATCH"),
678
695
  )
679
696
  add_cors_route(config, "/mapserv_proxy", "mapserver")
697
+ add_cors_route(config, "/mapserv_proxy/{ogcserver}/ogcapi/*path", "mapserver_ogcapi_mapserver")
698
+ add_cors_route(config, "/mapserv_proxy/{ogcserver}/wfs3/*path", "mapserver_ogcapi_qgisserver")
680
699
 
681
700
  # Add route to the tinyows proxy
682
701
  config.add_route("tinyowsproxy", "/tinyows_proxy", pregenerator=C2CPregenerator(role=True))
@@ -767,6 +786,7 @@ def includeme(config: pyramid.config.Configurator) -> None:
767
786
  add_cors_route(config, "/short/create", "shortener")
768
787
  config.add_route("shortener_create", "/short/create", request_method="POST")
769
788
  config.add_route("shortener_get", "/s/{ref}", request_method="GET")
789
+ config.add_route("shortener_fetch", "/short/get/{ref}", request_method="GET")
770
790
 
771
791
  # Geometry processing
772
792
  config.add_route("difference", "/difference", request_method="POST")
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2014-2024, Camptocamp SA
1
+ # Copyright (c) 2014-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -47,6 +47,7 @@ from pyramid.security import remember
47
47
  from pyramid_multiauth import MultiAuthenticationPolicy
48
48
  from zope.interface import implementer
49
49
 
50
+ import c2cgeoportal_commons.models
50
51
  from c2cgeoportal_geoportal.lib import oauth2
51
52
  from c2cgeoportal_geoportal.resources import defaultgroupsfinder
52
53
 
@@ -147,6 +148,8 @@ class OAuth2AuthenticationPolicy(CallbackAuthenticationPolicy): # type: ignore
147
148
  _LOG.debug("OAuth verify_request: %s", valid)
148
149
  if valid:
149
150
  request.user_ = oauth2_request.user
151
+ if request.user_ is not None and c2cgeoportal_commons.models.DBSession is not None:
152
+ c2cgeoportal_commons.models.DBSession.add(request.user_)
150
153
 
151
154
  return cast(str, request.user.username)
152
155
  return None
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2021, Camptocamp SA
1
+ # Copyright (c) 2013-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2012-2024, Camptocamp SA
1
+ # Copyright (c) 2012-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2014-2024, Camptocamp SA
1
+ # Copyright (c) 2014-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2020-2023, Camptocamp SA
1
+ # Copyright (c) 2020-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2024, Camptocamp SA
1
+ # Copyright (c) 2013-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2020-2023, Camptocamp SA
1
+ # Copyright (c) 2020-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2018-2024, Camptocamp SA
1
+ # Copyright (c) 2018-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2019-2024, Camptocamp SA
1
+ # Copyright (c) 2019-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2018-2023, Camptocamp SA
1
+ # Copyright (c) 2018-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021-2024, Camptocamp SA
1
+ # Copyright (c) 2021-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2024, Camptocamp SA
1
+ # Copyright (c) 2024-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -28,10 +28,10 @@
28
28
  import datetime
29
29
  import json
30
30
  import logging
31
+ import string
31
32
  from typing import TYPE_CHECKING, Any, NamedTuple, Optional, TypedDict, Union
32
33
 
33
34
  import pyramid.request
34
- import pyramid.response
35
35
  import simple_openid_connect.client
36
36
  import simple_openid_connect.data
37
37
  from pyramid.httpexceptions import HTTPBadRequest, HTTPInternalServerError, HTTPUnauthorized
@@ -77,7 +77,7 @@ def get_oidc_client(request: pyramid.request.Request, host: str) -> simple_openi
77
77
  url=openid_connect["url"],
78
78
  authentication_redirect_uri=request.route_url("oidc_callback"),
79
79
  client_id=openid_connect["client_id"],
80
- client_secret=openid_connect.get("client-secret"),
80
+ client_secret=openid_connect.get("client_secret"),
81
81
  scope=" ".join(openid_connect.get("scopes", ["openid", "profile", "email"])),
82
82
  )
83
83
 
@@ -87,9 +87,7 @@ class OidcRememberObject(TypedDict):
87
87
  The JSON object that is stored in a cookie to remember the user.
88
88
  """
89
89
 
90
- access_token: str
91
90
  access_token_expires: str
92
- refresh_token: str | None
93
91
  refresh_token_expires: str | None
94
92
  username: str | None
95
93
  display_name: str | None
@@ -124,16 +122,31 @@ def get_remember_from_user_info(
124
122
  ("settings_role", None),
125
123
  ("roles", None),
126
124
  ):
127
- user_info_field = settings_fields.get(field_, default_field)
128
- if user_info_field is not None:
129
- if user_info_field not in user_info:
130
- _LOG.error(
131
- "Field '%s' not found in user info, available: %s.",
132
- user_info_field,
133
- ", ".join(user_info.keys()),
134
- )
135
- raise HTTPInternalServerError(f"Field '{user_info_field}' not found in user info.")
136
- remember_object[field_] = user_info[user_info_field] # type: ignore[literal-required]
125
+ _LOG.debug("User info keys: %s", ", ".join(user_info.keys()))
126
+ user_info_field_template = settings_fields.get(field_, default_field)
127
+ if user_info_field_template is not None:
128
+ if "{" in user_info_field_template:
129
+ formatter = string.Formatter()
130
+ attributes = formatter.parse(user_info_field_template)
131
+ user_info_fields = [e[1] for e in attributes if e[1] is not None]
132
+ else:
133
+ user_info_fields = [user_info_field_template]
134
+
135
+ for user_info_field in user_info_fields:
136
+ if user_info_field not in user_info:
137
+ _LOG.error(
138
+ "Field '%s' not found in user info, available: %s.",
139
+ user_info_field,
140
+ ", ".join(user_info.keys()),
141
+ )
142
+ raise HTTPInternalServerError(f"Field '{user_info_field}' not found in user info.")
143
+
144
+ user_info_value = (
145
+ user_info_field_template.format(**user_info)
146
+ if "{" in user_info_field_template
147
+ else user_info[user_info_field_template]
148
+ )
149
+ remember_object[field_] = user_info_value # type: ignore[literal-required]
137
150
 
138
151
 
139
152
  def get_user_from_remember(
@@ -155,60 +168,66 @@ def get_user_from_remember(
155
168
  assert models.DBSession is not None
156
169
 
157
170
  user: static.User | DynamicUser | None
158
- username = remember_object["username"]
159
- assert username is not None
160
- email = remember_object["email"]
161
- assert email is not None
162
- display_name = remember_object["display_name"] or email
163
-
164
- openid_connect_configuration = request.registry.settings.get("authentication", {}).get(
165
- "openid_connect", {}
166
- )
167
- provide_roles = openid_connect_configuration.get("provide_roles", False)
168
- if provide_roles is False:
169
- user_query = models.DBSession.query(static.User)
170
- match_field = openid_connect_configuration.get("match_field", "username")
171
- if match_field == "username":
172
- user_query = user_query.filter_by(username=username)
173
- elif match_field == "email":
174
- user_query = user_query.filter_by(email=email)
171
+ try:
172
+ username = remember_object["username"]
173
+ assert username is not None
174
+ email = remember_object["email"]
175
+ assert email is not None
176
+ display_name = remember_object["display_name"] or email
177
+
178
+ openid_connect_configuration = request.registry.settings.get("authentication", {}).get(
179
+ "openid_connect", {}
180
+ )
181
+ provide_roles = openid_connect_configuration.get("provide_roles", False)
182
+ if provide_roles is False:
183
+ user_query = models.DBSession.query(static.User)
184
+ match_field = openid_connect_configuration.get("match_field", "username")
185
+ if match_field == "username":
186
+ user_query = user_query.filter_by(username=username)
187
+ elif match_field == "email":
188
+ user_query = user_query.filter_by(email=email)
189
+ else:
190
+ raise HTTPInternalServerError(
191
+ f"Unknown match_field: '{match_field}', allowed values are 'username' and 'email'"
192
+ )
193
+ user = user_query.one_or_none()
194
+ if update_create_user is True:
195
+ if user is not None:
196
+ for field in openid_connect_configuration.get("update_fields", []):
197
+ if field == "username":
198
+ user.username = username
199
+ elif field == "display_name":
200
+ user.display_name = display_name
201
+ elif field == "email":
202
+ user.email = email
203
+ else:
204
+ raise HTTPInternalServerError(
205
+ f"Unknown update_field: '{field}', allowed values are 'username', 'display_name' and 'email'"
206
+ )
207
+ elif openid_connect_configuration.get("create_user", False) is True:
208
+ user = static.User(username=username, email=email, display_name=display_name)
209
+ models.DBSession.add(user)
175
210
  else:
176
- raise HTTPInternalServerError(
177
- f"Unknown match_field: '{match_field}', allowed values are 'username' and 'email'"
211
+ roles = []
212
+ role_names = remember_object.get("roles", [])
213
+ if role_names:
214
+ query = models.DBSession.query(main.Role).filter(main.Role.name.in_(role_names))
215
+ roles = query.all()
216
+ user = DynamicUser(
217
+ id=-1,
218
+ username=username,
219
+ display_name=display_name,
220
+ email=email,
221
+ settings_role=(
222
+ models.DBSession.query(main.Role).filter_by(name=remember_object["settings_role"]).first()
223
+ if remember_object.get("settings_role") is not None
224
+ else None
225
+ ),
226
+ roles=roles,
178
227
  )
179
- user = user_query.one_or_none()
180
- if update_create_user is True:
181
- if user is not None:
182
- for field in openid_connect_configuration.get("update_fields", []):
183
- if field == "username":
184
- user.username = username
185
- elif field == "display_name":
186
- user.display_name = display_name
187
- elif field == "email":
188
- user.email = email
189
- else:
190
- raise HTTPInternalServerError(
191
- f"Unknown update_field: '{field}', allowed values are 'username', 'display_name' and 'email'"
192
- )
193
- elif openid_connect_configuration.get("create_user", False) is True:
194
- user = static.User(username=username, email=email, display_name=display_name)
195
- models.DBSession.add(user)
196
- else:
197
- user = DynamicUser(
198
- id=-1,
199
- username=username,
200
- display_name=display_name,
201
- email=email,
202
- settings_role=(
203
- models.DBSession.query(main.Role).filter_by(name=remember_object["settings_role"]).first()
204
- if remember_object.get("settings_role") is not None
205
- else None
206
- ),
207
- roles=[
208
- models.DBSession.query(main.Role).filter_by(name=role).one()
209
- for role in remember_object.get("roles", [])
210
- ],
211
- )
228
+ except KeyError:
229
+ _LOG.exception("Missing field in remember object")
230
+ return None
212
231
  return user
213
232
 
214
233
 
@@ -248,17 +267,36 @@ class OidcRemember:
248
267
  raise HTTPUnauthorized("See server logs for details")
249
268
 
250
269
  openid_connect = self.authentication_settings.get("openid_connect", {})
270
+ self.request.response.set_cookie(
271
+ "access_token",
272
+ token_response.access_token,
273
+ max_age=token_response.expires_in,
274
+ secure=True,
275
+ httponly=True,
276
+ samesite="Lax",
277
+ domain=self.request.domain,
278
+ )
279
+ if token_response.refresh_expires_in is not None:
280
+ self.request.response.set_cookie(
281
+ "refresh_token",
282
+ token_response.refresh_token,
283
+ max_age=token_response.refresh_expires_in,
284
+ secure=True,
285
+ httponly=True,
286
+ samesite="Lax",
287
+ domain=self.request.domain,
288
+ )
251
289
  remember_object: OidcRememberObject = {
252
- "access_token": token_response.access_token,
253
290
  "access_token_expires": (
254
- datetime.datetime.now() + datetime.timedelta(seconds=token_response.expires_in)
291
+ datetime.datetime.now(datetime.timezone.utc)
292
+ + datetime.timedelta(seconds=token_response.expires_in)
255
293
  ).isoformat(),
256
- "refresh_token": token_response.refresh_token,
257
294
  "refresh_token_expires": (
258
295
  None
259
296
  if token_response.refresh_expires_in is None
260
297
  else (
261
- datetime.datetime.now() + datetime.timedelta(seconds=token_response.refresh_expires_in)
298
+ datetime.datetime.now(datetime.timezone.utc)
299
+ + datetime.timedelta(seconds=token_response.refresh_expires_in)
262
300
  ).isoformat()
263
301
  ),
264
302
  "username": None,
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2024, Camptocamp SA
1
+ # Copyright (c) 2013-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2018-2024, Camptocamp SA
1
+ # Copyright (c) 2018-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2023, Camptocamp SA
1
+ # Copyright (c) 2011-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,15 +1,4 @@
1
- # yaml-language-server: $schema=https://raw.githubusercontent.com/camptocamp/c2cciutils/master/c2cciutils/schema.json
2
-
3
- checks:
4
- black: False
5
- isort: False
6
- prettier: False
7
- codespell: False
8
- eof: False
9
- required_workflows: False
10
- dependabot_config: False
11
- prospector_config: False
12
- setup: False
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/camptocamp/c2cciutils/1.7/c2cciutils/schema.json
13
2
 
14
3
  version:
15
4
  branch_to_version_re:
@@ -19,7 +19,7 @@ RUN make build
19
19
  RUN mv webpack.apps.js webpack.apps.js.tmpl
20
20
 
21
21
  ENTRYPOINT [ "/usr/bin/eval-templates" ]
22
- CMD [ "webpack", "serve", "--open", "--mode=development", "--watch", "--no-inline" ]
22
+ CMD [ "webpack", "serve", "--open", "--mode=development" ]
23
23
 
24
24
  ###############################################################################
25
25
 
@@ -33,8 +33,8 @@ WORKDIR /app
33
33
  COPY . /app
34
34
  # Workaround, see:https://github.com/moby/moby/issues/37965
35
35
  RUN true
36
- COPY --from=builder /opt/c2cgeoportal/geoportal/node_modules/ngeo/dist/* /etc/static-ngeo/
37
- RUN rm /etc/static-ngeo/*.html
36
+ COPY --from=builder /opt/c2cgeoportal/geoportal/node_modules/ngeo/dist/vendor.* /etc/static-ngeo/
37
+ COPY --from=builder /opt/c2cgeoportal/geoportal/node_modules/ngeo/dist/*_alt.* /etc/static-ngeo/
38
38
  COPY --from=builder /etc/static-ngeo/* /etc/static-ngeo/
39
39
  COPY --from=builder /app/alembic.ini /app/alembic.yaml ./
40
40
  RUN chmod go+w /etc/static-ngeo/
@@ -1,5 +1,5 @@
1
1
  # Language provided by the application
2
- LANGUAGES ?= en fr de
2
+ LANGUAGES ?= en fr de it rm
3
3
  NGEO_INTERFACES ?= desktop mobile iframe_api
4
4
  NGEO_API ?= TRUE
5
5
 
@@ -44,13 +44,13 @@ qualname = alembic
44
44
 
45
45
  [handler_console]
46
46
  class = StreamHandler
47
- kwargs = {'stream': 'ext://sys.stderr'}
47
+ args = (sys.stderr,)
48
48
  level = NOTSET
49
49
  formatter = generic
50
50
 
51
51
  [handler_json]
52
52
  class = c2cwsgiutils.pyramid_logging.JsonLogHandler
53
- kwargs = {'stream': 'ext://sys.stdout'}
53
+ args = (sys.stderr,)
54
54
  level = NOTSET
55
55
 
56
56
  [formatter_generic]
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2019-2024, Camptocamp SA
1
+ # Copyright (c) 2019-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -42,10 +42,20 @@ module.exports = {
42
42
  publicPath: devServer ? '${VISIBLE_ENTRY_POINT}dev/' : '.__ENTRY_POINT__static-ngeo/',
43
43
  },
44
44
  devServer: {
45
- publicPath: '${VISIBLE_WEB_PROTOCOL}://${VISIBLE_WEB_HOST}${VISIBLE_ENTRY_POINT}dev/',
45
+ devMiddleware: {
46
+ publicPath: '${VISIBLE_WEB_PROTOCOL}://${VISIBLE_WEB_HOST}${VISIBLE_ENTRY_POINT}dev/',
47
+ },
46
48
  port: 8080,
47
49
  host: 'webpack_dev_server',
48
50
  hot: true,
51
+ compress: false,
52
+ client: {
53
+ webSocketURL: {
54
+ hostname: 'localhost',
55
+ port: 8080,
56
+ protocol: 'ws',
57
+ },
58
+ },
49
59
  },
50
60
  entry: entry,
51
61
  plugins: plugins,
@@ -5,6 +5,8 @@
5
5
  "extent": "",
6
6
  "extent_mapserver": "",
7
7
  "authtkt_secret": "",
8
+ "tilecloud_chain_session_secret": "",
9
+ "tilecloud_chain_session_salt": "",
8
10
  "unsafe_long_version": false,
9
11
  "geomapfish_version": "",
10
12
  "geomapfish_main_version": "",
@@ -5,6 +5,8 @@
5
5
  "extent": "",
6
6
  "extent_mapserver": "",
7
7
  "authtkt_secret": "",
8
+ "tilecloud_chain_session_secret": "",
9
+ "tilecloud_chain_session_salt": "",
8
10
  "unsafe_long_version": false,
9
11
  "geomapfish_version": "",
10
12
  "geomapfish_main_version": "",
@@ -6,7 +6,6 @@ on:
6
6
 
7
7
  # To publish the images to be used on Kubernetes
8
8
  # env:
9
- # PROJECT: {{cookiecutter.package}}
10
9
  # HAS_SECRETS: ${{'{{'}} secrets.HAS_SECRETS }}
11
10
 
12
11
  jobs:
@@ -19,6 +18,7 @@ jobs:
19
18
  - uses: actions/checkout@v4
20
19
 
21
20
  # To publish the images to be used on Kubernetes
21
+ # Requires CI_GPG_PRIVATE_KEY and GOPASS_CI_GITHUB_TOKEN secrets.
22
22
  # - uses: camptocamp/initialise-gopass-summon-action@v2
23
23
  # with:
24
24
  # ci-gpg-private-key: ${{'{{'}} secrets.CI_GPG_PRIVATE_KEY }}
@@ -5,13 +5,6 @@ on:
5
5
  schedule:
6
6
  - cron: '30 2 * * *'
7
7
 
8
- env:
9
- PROJECT: {{cookiecutter.package}}
10
- # Requires CI_GPG_PRIVATE_KEY and GOPASS_CI_GITHUB_TOKEN secrets.
11
- # OPENSHIFT_PROJECT: gs-gmf-{{cookiecutter.package}}
12
- # The release branches
13
- HELM_RELEASE_NAMES: int-{{cookiecutter.geomapfish_main_version_dash}},prod-{{cookiecutter.geomapfish_main_version_dash}}
14
-
15
8
  jobs:
16
9
  rebuild:
17
10
  runs-on: ubuntu-24.04
@@ -30,6 +23,7 @@ jobs:
30
23
  with:
31
24
  ref: ${{'{{'}} matrix.branch }}
32
25
 
26
+ # Requires CI_GPG_PRIVATE_KEY and GOPASS_CI_GITHUB_TOKEN secrets.
33
27
  - uses: camptocamp/initialise-gopass-summon-action@v2
34
28
  with:
35
29
  ci-gpg-private-key: ${{'{{'}} secrets.CI_GPG_PRIVATE_KEY }}