c2cgeoportal-commons 2.5.0.100__py3-none-any.whl → 2.9rc44__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.
Potentially problematic release.
This version of c2cgeoportal-commons might be problematic. Click here for more details.
- c2cgeoportal_commons/__init__.py +2 -5
- c2cgeoportal_commons/alembic/env.py +52 -33
- c2cgeoportal_commons/alembic/main/028477929d13_add_technical_roles.py +10 -7
- c2cgeoportal_commons/alembic/main/04f05bfbb05e_remove_the_old_is_expanded_column.py +62 -0
- c2cgeoportal_commons/alembic/main/116b9b79fc4d_internal_and_external_layer_tables_.py +42 -43
- c2cgeoportal_commons/alembic/main/1418cb05921b_merge_1_6_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/164ac0819a61_add_image_format_to_wmts_layer.py +9 -6
- c2cgeoportal_commons/alembic/main/166ff2dcc48d_create_database.py +22 -25
- c2cgeoportal_commons/alembic/main/16e43f8c0330_remove_old_metadata_column.py +9 -6
- c2cgeoportal_commons/alembic/main/1d5d4abfebd1_add_restricted_theme.py +14 -8
- c2cgeoportal_commons/alembic/main/1de20166b274_remove_v1_artifacts.py +9 -6
- c2cgeoportal_commons/alembic/main/20137477bd02_update_icons_url.py +9 -6
- c2cgeoportal_commons/alembic/main/21f11066f8ec_trigger_on_role_updates_user_in_static.py +21 -20
- c2cgeoportal_commons/alembic/main/22e6dfb556de_add_description_tree_.py +9 -6
- c2cgeoportal_commons/alembic/main/29f2a32859ec_merge_1_6_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/2b8ed8c1df94_set_layergroup_treeitem_is_as_a_primary_.py +9 -6
- c2cgeoportal_commons/alembic/main/2e57710fecfe_update_the_ogc_server_for_ogc_api.py +71 -0
- c2cgeoportal_commons/alembic/main/32527659d57b_move_exclude_properties_from_layerv1_to_.py +15 -12
- c2cgeoportal_commons/alembic/main/32b21aa1d0ed_merge_e004f76e951a_and_e004f76e951a_.py +9 -8
- c2cgeoportal_commons/alembic/main/338b57593823_remove_trigger_on_role_name_change.py +25 -24
- c2cgeoportal_commons/alembic/main/415746eb9f6_changes_for_v2.py +45 -43
- c2cgeoportal_commons/alembic/main/44c91d82d419_add_table_log.py +72 -0
- c2cgeoportal_commons/alembic/main/5109242131ce_add_column_time_widget.py +11 -9
- c2cgeoportal_commons/alembic/main/52916d8fde8b_add_sql_fields_to_vector_tiles.py +60 -0
- c2cgeoportal_commons/alembic/main/53ba1a68d5fe_add_theme_to_fulltextsearch.py +9 -6
- c2cgeoportal_commons/alembic/main/54645a535ad6_add_ordering_in_relation.py +17 -14
- c2cgeoportal_commons/alembic/main/56dc90838d90_fix_removing_layerv1.py +10 -8
- c2cgeoportal_commons/alembic/main/596ba21e3833_separate_local_internal.py +13 -14
- c2cgeoportal_commons/alembic/main/678f88c7ad5e_add_vector_tiles_layers_table.py +9 -6
- c2cgeoportal_commons/alembic/main/6a412d9437b1_rename_serverogc_to_ogcserver.py +9 -6
- c2cgeoportal_commons/alembic/main/6d87fdad275a_convert_metadata_to_the_right_case.py +13 -20
- c2cgeoportal_commons/alembic/main/7530011a66a7_trigger_on_role_updates_user_in_static.py +21 -20
- c2cgeoportal_commons/alembic/main/78fd093c8393_add_api_s_intrfaces.py +36 -52
- c2cgeoportal_commons/alembic/main/7d271f4527cd_add_layer_column_in_layerv1_table.py +9 -6
- c2cgeoportal_commons/alembic/main/809650bd04c3_add_dimension_field.py +9 -6
- c2cgeoportal_commons/alembic/main/8117bb9bba16_use_dimension_on_all_the_layers.py +9 -6
- c2cgeoportal_commons/alembic/main/87f8330ed64e_add_missing_delete_cascades.py +305 -0
- c2cgeoportal_commons/alembic/main/9268a1dffac0_add_trigger_to_be_able_to_correctly_.py +11 -8
- c2cgeoportal_commons/alembic/main/94db7e7e5b21_merge_2_2_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/951ff84bd8ec_be_able_to_delete_a_wms_layer_in_sql.py +9 -6
- c2cgeoportal_commons/alembic/main/a00109812f89_add_field_layer_wms_valid.py +60 -0
- c2cgeoportal_commons/alembic/main/a4558f032d7d_add_support_of_cog_layers.py +74 -0
- c2cgeoportal_commons/alembic/main/a4f1aac9bda_merge_1_6_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/b60f2a505f42_remame_uimetadata_to_metadata.py +9 -6
- c2cgeoportal_commons/alembic/main/b6b09f414fe8_sync_model_database.py +165 -0
- c2cgeoportal_commons/alembic/main/c75124553bf3_remove_deprecated_columns.py +9 -6
- c2cgeoportal_commons/alembic/main/d48a63b348f1_change_mapserver_url_for_docker.py +13 -14
- c2cgeoportal_commons/alembic/main/d8ef99bc227e_be_able_to_delete_a_linked_functionality.py +16 -13
- c2cgeoportal_commons/alembic/main/daf738d5bae4_merge_2_0_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/main/dba87f2647f9_merge_2_2_on_2_3.py +9 -8
- c2cgeoportal_commons/alembic/main/e004f76e951a_add_missing_not_null.py +15 -32
- c2cgeoportal_commons/alembic/main/e7e03dedade3_put_the_default_wms_server_in_the_.py +13 -14
- c2cgeoportal_commons/alembic/main/e85afd327ab3_cascade_deletes_to_tsearch.py +9 -6
- c2cgeoportal_commons/alembic/main/ec82a8906649_add_missing_on_delete_cascade_on_layer_.py +13 -10
- c2cgeoportal_commons/alembic/main/ee25d267bf46_main_interface_desktop.py +11 -14
- c2cgeoportal_commons/alembic/main/eeb345672454_merge_2_4_and_master_branches.py +9 -8
- c2cgeoportal_commons/alembic/static/0c640a58a09a_add_opt_key_column.py +9 -6
- c2cgeoportal_commons/alembic/static/107b81f5b9fe_add_missing_delete_cascades.py +78 -0
- c2cgeoportal_commons/alembic/static/1857owc78a07_add_last_login_and_expiration_date.py +7 -6
- c2cgeoportal_commons/alembic/static/1da396a88908_move_user_table_to_static_schema.py +19 -20
- c2cgeoportal_commons/alembic/static/267b4c1bde2e_add_display_name_in_the_user_for_better_.py +62 -0
- c2cgeoportal_commons/alembic/static/3f89a7d71a5e_alter_column_url_to_remove_limitation.py +8 -7
- c2cgeoportal_commons/alembic/static/44c91d82d419_add_table_log.py +72 -0
- c2cgeoportal_commons/alembic/static/53d671b17b20_add_timezone_on_datetime_fields.py +9 -6
- c2cgeoportal_commons/alembic/static/5472fbc19f39_add_temp_password_column.py +9 -6
- c2cgeoportal_commons/alembic/static/76d72fb3fcb9_add_oauth2_pkce.py +70 -0
- c2cgeoportal_commons/alembic/static/7ef947f30f20_add_oauth2_tables.py +115 -0
- c2cgeoportal_commons/alembic/static/910b4ca53b68_sync_model_database.py +186 -0
- c2cgeoportal_commons/alembic/static/aa41e9613256_wip_add_openid_connect_support.py +64 -0
- c2cgeoportal_commons/alembic/static/ae5e88f35669_add_table_user_role.py +15 -18
- c2cgeoportal_commons/alembic/static/bd029dbfc11a_fill_tech_data_column.py +10 -8
- c2cgeoportal_commons/lib/email_.py +16 -18
- c2cgeoportal_commons/lib/literal.py +44 -0
- c2cgeoportal_commons/lib/url.py +221 -0
- c2cgeoportal_commons/lib/validators.py +2 -4
- c2cgeoportal_commons/models/__init__.py +19 -15
- c2cgeoportal_commons/models/main.py +1268 -266
- c2cgeoportal_commons/models/sqlalchemy.py +15 -19
- c2cgeoportal_commons/models/static.py +338 -63
- c2cgeoportal_commons/py.typed +0 -0
- c2cgeoportal_commons/testing/__init__.py +18 -13
- c2cgeoportal_commons/testing/initializedb.py +15 -14
- {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/METADATA +18 -23
- c2cgeoportal_commons-2.9rc44.dist-info/RECORD +89 -0
- {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/WHEEL +1 -1
- tests/conftest.py +4 -6
- c2cgeoportal_commons-2.5.0.100.dist-info/RECORD +0 -75
- tests/test_interface.py +0 -27
- tests/test_roles.py +0 -63
- tests/test_users.py +0 -71
- tests/test_validators.py +0 -20
- {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2012-2020, Camptocamp SA
|
|
1
|
+
# Copyright (c) 2012-2024, Camptocamp SA
|
|
4
2
|
# All rights reserved.
|
|
5
3
|
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -27,7 +25,7 @@
|
|
|
27
25
|
# of the authors and should not be interpreted as representing official policies,
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
|
29
27
|
import json
|
|
30
|
-
from typing import Any
|
|
28
|
+
from typing import Any
|
|
31
29
|
|
|
32
30
|
from sqlalchemy.engine import Dialect
|
|
33
31
|
from sqlalchemy.types import VARCHAR, TypeDecorator, UserDefinedType
|
|
@@ -35,37 +33,35 @@ from sqlalchemy.types import VARCHAR, TypeDecorator, UserDefinedType
|
|
|
35
33
|
|
|
36
34
|
# get from https://docs.sqlalchemy.org/en/latest/orm/extensions/
|
|
37
35
|
# mutable.html#establishing-mutability-on-scalar-column-values
|
|
38
|
-
class JSONEncodedDict(TypeDecorator):
|
|
39
|
-
"""
|
|
40
|
-
Represents an immutable structure as a json-encoded string.
|
|
41
|
-
"""
|
|
36
|
+
class JSONEncodedDict(TypeDecorator[Any]):
|
|
37
|
+
"""Represent an immutable structure as a json-encoded string."""
|
|
42
38
|
|
|
43
39
|
impl = VARCHAR
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
def process_bind_param(value: Optional[dict], _: Dialect) -> Optional[str]:
|
|
41
|
+
def process_bind_param(self, value: dict[str, Any] | None, _: Dialect) -> str | None:
|
|
47
42
|
return json.dumps(value) if value is not None else None
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
def process_result_value(value: Optional[str], _: Dialect) -> Optional[dict]:
|
|
44
|
+
def process_result_value(self, value: str | None, _: Dialect) -> dict[str, Any] | None:
|
|
51
45
|
return json.loads(value) if value is not None else None
|
|
52
46
|
|
|
53
47
|
@property
|
|
54
|
-
def python_type(self) ->
|
|
48
|
+
def python_type(self) -> type[Any]:
|
|
55
49
|
return dict
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
def process_literal_param(self, value: Any | None, dialect: Any) -> str:
|
|
52
|
+
assert isinstance(value, str)
|
|
59
53
|
del dialect
|
|
60
54
|
return json.dumps(value)
|
|
61
55
|
|
|
62
56
|
|
|
63
|
-
class TsVector(UserDefinedType):
|
|
64
|
-
"""
|
|
57
|
+
class TsVector(UserDefinedType[dict[str, str]]): # pylint: disable=abstract-method
|
|
58
|
+
"""A custom type for PostgreSQL's tsvector type."""
|
|
59
|
+
|
|
60
|
+
cache_ok = True
|
|
65
61
|
|
|
66
|
-
def get_col_spec(self) -> str:
|
|
62
|
+
def get_col_spec(self) -> str:
|
|
67
63
|
return "TSVECTOR"
|
|
68
64
|
|
|
69
65
|
@property
|
|
70
|
-
def python_type(self) ->
|
|
66
|
+
def python_type(self) -> type[Any]:
|
|
71
67
|
return dict
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2011-2020, Camptocamp SA
|
|
1
|
+
# Copyright (c) 2011-2024, Camptocamp SA
|
|
4
2
|
# All rights reserved.
|
|
5
3
|
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -29,31 +27,35 @@
|
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
import crypt
|
|
32
|
-
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
33
|
from hashlib import sha1
|
|
34
34
|
from hmac import compare_digest as compare_hash
|
|
35
|
-
import
|
|
36
|
-
from typing import Any, List
|
|
35
|
+
from typing import Any
|
|
37
36
|
|
|
37
|
+
import sqlalchemy.schema
|
|
38
38
|
from c2c.template.config import config
|
|
39
|
-
import pytz
|
|
40
39
|
from sqlalchemy import Column, ForeignKey, Table
|
|
41
40
|
from sqlalchemy.dialects.postgresql import HSTORE
|
|
42
41
|
from sqlalchemy.ext.mutable import MutableDict
|
|
43
|
-
from sqlalchemy.orm import backref, relationship
|
|
42
|
+
from sqlalchemy.orm import Mapped, backref, mapped_column, relationship
|
|
44
43
|
from sqlalchemy.types import Boolean, DateTime, Integer, String, Unicode
|
|
45
44
|
|
|
45
|
+
from c2cgeoportal_commons.lib.literal import Literal
|
|
46
46
|
from c2cgeoportal_commons.models import Base, _
|
|
47
|
-
from c2cgeoportal_commons.models.main import Role
|
|
47
|
+
from c2cgeoportal_commons.models.main import AbstractLog, Role
|
|
48
48
|
|
|
49
49
|
try:
|
|
50
|
-
from colander import drop, Email
|
|
51
|
-
from deform.widget import HiddenWidget, DateTimeInputWidget
|
|
52
50
|
from c2cgeoform.ext.deform_ext import RelationSelect2Widget
|
|
51
|
+
from colander import Email, drop
|
|
52
|
+
from deform.widget import DateTimeInputWidget, HiddenWidget
|
|
53
53
|
except ModuleNotFoundError:
|
|
54
|
-
drop = None
|
|
54
|
+
drop = None # pylint: disable=invalid-name
|
|
55
55
|
|
|
56
56
|
class GenericClass:
|
|
57
|
+
"""Generic class."""
|
|
58
|
+
|
|
57
59
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
58
60
|
pass
|
|
59
61
|
|
|
@@ -61,10 +63,11 @@ except ModuleNotFoundError:
|
|
|
61
63
|
HiddenWidget = GenericClass
|
|
62
64
|
DateTimeInputWidget = GenericClass
|
|
63
65
|
CollenderGeometry = GenericClass
|
|
64
|
-
RelationSelect2Widget = GenericClass
|
|
66
|
+
RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
_LOG = logging.getLogger(__name__)
|
|
70
|
+
_OPENID_CONNECT_ENABLED = os.environ.get("OPENID_CONNECT_ENABLED", "false").lower() in ("true", "yes", "1")
|
|
68
71
|
|
|
69
72
|
_schema: str = config["schema_static"] or "static"
|
|
70
73
|
|
|
@@ -72,44 +75,130 @@ _schema: str = config["schema_static"] or "static"
|
|
|
72
75
|
user_role = Table(
|
|
73
76
|
"user_role",
|
|
74
77
|
Base.metadata,
|
|
75
|
-
Column("user_id", Integer, ForeignKey(_schema + ".user.id"), primary_key=True),
|
|
78
|
+
Column("user_id", Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), primary_key=True),
|
|
76
79
|
Column("role_id", Integer, primary_key=True),
|
|
77
80
|
schema=_schema,
|
|
78
81
|
)
|
|
79
82
|
|
|
80
83
|
|
|
81
|
-
class User(Base):
|
|
84
|
+
class User(Base): # type: ignore
|
|
85
|
+
"""The user table representation."""
|
|
86
|
+
|
|
82
87
|
__tablename__ = "user"
|
|
83
88
|
__table_args__ = {"schema": _schema}
|
|
84
|
-
__colanderalchemy_config__ = {
|
|
89
|
+
__colanderalchemy_config__ = {
|
|
90
|
+
"title": _("User"),
|
|
91
|
+
"plural": _("Users"),
|
|
92
|
+
"description": Literal(
|
|
93
|
+
_(
|
|
94
|
+
"""
|
|
95
|
+
<div class="help-block">
|
|
96
|
+
<p>Each user may have from 1 to n roles, but each user has a default role from
|
|
97
|
+
which are taken some settings. The default role (defined through the
|
|
98
|
+
"Settings from role" selection) has an influence on the role extent and on some
|
|
99
|
+
functionalities regarding their configuration.</p>
|
|
100
|
+
|
|
101
|
+
<p>Role extents for users can only be set in one role, because the application
|
|
102
|
+
is currently not able to check multiple extents for one user, thus it is the
|
|
103
|
+
default role which defines this unique extent.</p>
|
|
104
|
+
|
|
105
|
+
<p>Any functionality specified as <b>single</b> can be defined only once per user.
|
|
106
|
+
Hence, these functionalities have to be defined in the default role.</p>
|
|
107
|
+
|
|
108
|
+
<p>By default, functionalities are not specified as <b>single</b>. Currently, the
|
|
109
|
+
following functionalities are of <b>single</b> type:</p>
|
|
110
|
+
|
|
111
|
+
<ul>
|
|
112
|
+
<li><code>default_basemap</code></li>
|
|
113
|
+
<li><code>default_theme</code></li>
|
|
114
|
+
<li><code>preset_layer_filter</code></li>
|
|
115
|
+
<li><code>open_panel</code></li>
|
|
116
|
+
</ul>
|
|
117
|
+
|
|
118
|
+
<p>Any other functionality (with <b>single</b> not set or set to <code>false</code>) can
|
|
119
|
+
be defined in any role linked to the user.</p>
|
|
120
|
+
<hr>
|
|
121
|
+
</div>
|
|
122
|
+
"""
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
}
|
|
85
126
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
86
|
-
item_type =
|
|
127
|
+
item_type: Mapped[str] = mapped_column(
|
|
87
128
|
"type", String(10), nullable=False, info={"colanderalchemy": {"widget": HiddenWidget()}}
|
|
88
129
|
)
|
|
89
130
|
__mapper_args__ = {"polymorphic_on": item_type, "polymorphic_identity": "user"}
|
|
90
131
|
|
|
91
|
-
id
|
|
92
|
-
|
|
93
|
-
|
|
132
|
+
id: Mapped[int] = mapped_column(
|
|
133
|
+
Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
|
|
134
|
+
)
|
|
135
|
+
username: Mapped[str] = mapped_column(
|
|
136
|
+
Unicode,
|
|
137
|
+
unique=True,
|
|
138
|
+
nullable=False,
|
|
139
|
+
info={
|
|
140
|
+
"colanderalchemy": (
|
|
141
|
+
{
|
|
142
|
+
"title": _("Username"),
|
|
143
|
+
"description": _("Name used for authentication (must be unique)."),
|
|
144
|
+
}
|
|
145
|
+
if not _OPENID_CONNECT_ENABLED
|
|
146
|
+
else {"widget": HiddenWidget()}
|
|
147
|
+
)
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
display_name: Mapped[str] = mapped_column(
|
|
151
|
+
Unicode,
|
|
152
|
+
info={
|
|
153
|
+
"colanderalchemy": {
|
|
154
|
+
"title": _("Display name"),
|
|
155
|
+
"description": _("Name displayed in the application."),
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
_password: Mapped[str] = mapped_column(
|
|
160
|
+
"password", Unicode, nullable=False, info={"colanderalchemy": {"exclude": True}}
|
|
94
161
|
)
|
|
95
|
-
|
|
96
|
-
temp_password = Column(
|
|
162
|
+
temp_password: Mapped[str | None] = mapped_column(
|
|
97
163
|
"temp_password", Unicode, nullable=True, info={"colanderalchemy": {"exclude": True}}
|
|
98
164
|
)
|
|
99
|
-
tech_data =
|
|
100
|
-
email =
|
|
101
|
-
Unicode,
|
|
165
|
+
tech_data = mapped_column(MutableDict.as_mutable(HSTORE), info={"colanderalchemy": {"exclude": True}}) # type: ignore[arg-type]
|
|
166
|
+
email: Mapped[str] = mapped_column(
|
|
167
|
+
Unicode,
|
|
168
|
+
nullable=False,
|
|
169
|
+
index=True,
|
|
170
|
+
info={
|
|
171
|
+
"colanderalchemy": {
|
|
172
|
+
"title": _("Email"),
|
|
173
|
+
"description": _(
|
|
174
|
+
"Used to send emails to the user, for example in case of password recovery."
|
|
175
|
+
),
|
|
176
|
+
"validator": Email(),
|
|
177
|
+
}
|
|
178
|
+
},
|
|
102
179
|
)
|
|
103
|
-
is_password_changed =
|
|
104
|
-
Boolean,
|
|
180
|
+
is_password_changed: Mapped[bool] = mapped_column(
|
|
181
|
+
Boolean,
|
|
182
|
+
default=False,
|
|
183
|
+
info={
|
|
184
|
+
"colanderalchemy": (
|
|
185
|
+
{
|
|
186
|
+
"title": _("The user changed his password"),
|
|
187
|
+
"description": _("Indicates if user has changed his password."),
|
|
188
|
+
}
|
|
189
|
+
if not _OPENID_CONNECT_ENABLED
|
|
190
|
+
else {"exclude": True}
|
|
191
|
+
)
|
|
192
|
+
},
|
|
105
193
|
)
|
|
106
194
|
|
|
107
|
-
settings_role_id =
|
|
108
|
-
Integer,
|
|
195
|
+
settings_role_id: Mapped[int] = mapped_column(
|
|
196
|
+
Integer(),
|
|
197
|
+
nullable=True,
|
|
109
198
|
info={
|
|
110
199
|
"colanderalchemy": {
|
|
111
200
|
"title": _("Settings from role"),
|
|
112
|
-
"description": "
|
|
201
|
+
"description": _("Used to get some settings for the user (not for permissions)."),
|
|
113
202
|
"widget": RelationSelect2Widget(
|
|
114
203
|
Role, "id", "name", order_by="name", default_value=("", _("- Select -"))
|
|
115
204
|
),
|
|
@@ -128,37 +217,86 @@ class User(Base):
|
|
|
128
217
|
Role,
|
|
129
218
|
secondary=user_role,
|
|
130
219
|
secondaryjoin=Role.id == user_role.c.role_id,
|
|
131
|
-
backref=backref(
|
|
132
|
-
|
|
220
|
+
backref=backref(
|
|
221
|
+
"users",
|
|
222
|
+
order_by="User.username",
|
|
223
|
+
info={
|
|
224
|
+
"colanderalchemy": {
|
|
225
|
+
"title": _("Users"),
|
|
226
|
+
"description": _("Users granted with this role."),
|
|
227
|
+
"exclude": True,
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
),
|
|
231
|
+
info={
|
|
232
|
+
"colanderalchemy": {
|
|
233
|
+
"title": _("Roles"),
|
|
234
|
+
"description": _("Roles granted to the user."),
|
|
235
|
+
"exclude": True,
|
|
236
|
+
}
|
|
237
|
+
},
|
|
133
238
|
)
|
|
134
239
|
|
|
135
|
-
last_login =
|
|
240
|
+
last_login: Mapped[datetime] = mapped_column(
|
|
136
241
|
DateTime(timezone=True),
|
|
242
|
+
nullable=True,
|
|
137
243
|
info={
|
|
138
|
-
"colanderalchemy":
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
244
|
+
"colanderalchemy": (
|
|
245
|
+
{
|
|
246
|
+
"title": _("Last login"),
|
|
247
|
+
"description": _("Date of the user's last login."),
|
|
248
|
+
"missing": drop,
|
|
249
|
+
"widget": DateTimeInputWidget(readonly=True),
|
|
250
|
+
}
|
|
251
|
+
if not _OPENID_CONNECT_ENABLED
|
|
252
|
+
else {"exclude": True}
|
|
253
|
+
)
|
|
143
254
|
},
|
|
144
255
|
)
|
|
145
256
|
|
|
146
|
-
expire_on
|
|
257
|
+
expire_on: Mapped[datetime | None] = mapped_column(
|
|
258
|
+
DateTime(timezone=True),
|
|
259
|
+
info={
|
|
260
|
+
"colanderalchemy": (
|
|
261
|
+
{
|
|
262
|
+
"title": _("Expiration date"),
|
|
263
|
+
"description": _("After this date the user will not be able to login anymore."),
|
|
264
|
+
}
|
|
265
|
+
if not _OPENID_CONNECT_ENABLED
|
|
266
|
+
else {"exclude": True}
|
|
267
|
+
)
|
|
268
|
+
},
|
|
269
|
+
)
|
|
147
270
|
|
|
148
|
-
deactivated
|
|
271
|
+
deactivated: Mapped[bool] = mapped_column(
|
|
272
|
+
Boolean,
|
|
273
|
+
default=False,
|
|
274
|
+
info={
|
|
275
|
+
"colanderalchemy": (
|
|
276
|
+
{
|
|
277
|
+
"title": _("Deactivated"),
|
|
278
|
+
"description": _("Deactivate a user without removing it completely."),
|
|
279
|
+
}
|
|
280
|
+
if not _OPENID_CONNECT_ENABLED
|
|
281
|
+
else {"exclude": True}
|
|
282
|
+
)
|
|
283
|
+
},
|
|
284
|
+
)
|
|
149
285
|
|
|
150
|
-
def __init__(
|
|
286
|
+
def __init__( # nosec
|
|
151
287
|
self,
|
|
152
288
|
username: str = "",
|
|
153
289
|
password: str = "",
|
|
154
290
|
email: str = "",
|
|
155
291
|
is_password_changed: bool = False,
|
|
156
|
-
settings_role: Role = None,
|
|
157
|
-
roles:
|
|
158
|
-
expire_on: datetime = None,
|
|
292
|
+
settings_role: Role | None = None,
|
|
293
|
+
roles: list[Role] | None = None,
|
|
294
|
+
expire_on: datetime | None = None,
|
|
159
295
|
deactivated: bool = False,
|
|
296
|
+
display_name: str | None = None,
|
|
160
297
|
) -> None:
|
|
161
298
|
self.username = username
|
|
299
|
+
self.display_name = display_name or username
|
|
162
300
|
self.password = password
|
|
163
301
|
self.tech_data = {}
|
|
164
302
|
self.email = email
|
|
@@ -171,16 +309,16 @@ class User(Base):
|
|
|
171
309
|
|
|
172
310
|
@property
|
|
173
311
|
def password(self) -> str:
|
|
174
|
-
"""
|
|
175
|
-
return self._password
|
|
312
|
+
"""Get the password."""
|
|
313
|
+
return self._password
|
|
176
314
|
|
|
177
315
|
@password.setter
|
|
178
316
|
def password(self, password: str) -> None:
|
|
179
|
-
"""
|
|
317
|
+
"""Encrypt password on the fly."""
|
|
180
318
|
self._password = self.__encrypt_password(password)
|
|
181
319
|
|
|
182
320
|
def set_temp_password(self, password: str) -> None:
|
|
183
|
-
"""
|
|
321
|
+
"""Encrypt password on the fly."""
|
|
184
322
|
self.temp_password = self.__encrypt_password(password)
|
|
185
323
|
|
|
186
324
|
@staticmethod
|
|
@@ -193,8 +331,8 @@ class User(Base):
|
|
|
193
331
|
return crypt.crypt(password, crypt.METHOD_SHA512)
|
|
194
332
|
|
|
195
333
|
def validate_password(self, passwd: str) -> bool:
|
|
196
|
-
"""
|
|
197
|
-
this method _MUST_ return a boolean.
|
|
334
|
+
"""
|
|
335
|
+
Check the password against existing credentials. this method _MUST_ return a boolean.
|
|
198
336
|
|
|
199
337
|
@param passwd: the password that was provided by the user to
|
|
200
338
|
try and authenticate. This is the clear text version that we will
|
|
@@ -213,7 +351,7 @@ class User(Base):
|
|
|
213
351
|
|
|
214
352
|
if (
|
|
215
353
|
self.temp_password is not None
|
|
216
|
-
and self.temp_password != ""
|
|
354
|
+
and self.temp_password != "" # nosec
|
|
217
355
|
and compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password))
|
|
218
356
|
):
|
|
219
357
|
self._password = self.temp_password
|
|
@@ -223,22 +361,159 @@ class User(Base):
|
|
|
223
361
|
return False
|
|
224
362
|
|
|
225
363
|
def expired(self) -> bool:
|
|
226
|
-
return self.expire_on is not None and self.expire_on < datetime.now(
|
|
364
|
+
return self.expire_on is not None and self.expire_on < datetime.now(timezone.utc)
|
|
227
365
|
|
|
228
366
|
def update_last_login(self) -> None:
|
|
229
|
-
self.last_login = datetime.now(
|
|
367
|
+
self.last_login = datetime.now(timezone.utc)
|
|
230
368
|
|
|
231
369
|
def __str__(self) -> str:
|
|
232
|
-
return self.username or ""
|
|
370
|
+
return self.username or ""
|
|
371
|
+
|
|
233
372
|
|
|
373
|
+
class Shorturl(Base): # type: ignore
|
|
374
|
+
"""The shorturl table representation."""
|
|
234
375
|
|
|
235
|
-
class Shorturl(Base):
|
|
236
376
|
__tablename__ = "shorturl"
|
|
237
377
|
__table_args__ = {"schema": _schema}
|
|
238
|
-
id =
|
|
239
|
-
url =
|
|
240
|
-
ref =
|
|
241
|
-
creator_email =
|
|
242
|
-
creation =
|
|
243
|
-
last_hit =
|
|
244
|
-
nb_hits =
|
|
378
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
379
|
+
url: Mapped[str] = mapped_column(Unicode)
|
|
380
|
+
ref: Mapped[str] = mapped_column(String(20), index=True, unique=True, nullable=False)
|
|
381
|
+
creator_email: Mapped[str | None] = mapped_column(Unicode(200), nullable=True)
|
|
382
|
+
creation: Mapped[datetime] = mapped_column(DateTime)
|
|
383
|
+
last_hit: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
|
384
|
+
nb_hits: Mapped[int] = mapped_column(Integer)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class OAuth2Client(Base): # type: ignore
|
|
388
|
+
"""The oauth2_client table representation."""
|
|
389
|
+
|
|
390
|
+
__tablename__ = "oauth2_client"
|
|
391
|
+
__table_args__ = {"schema": _schema}
|
|
392
|
+
__colanderalchemy_config__ = {"title": _("OAuth2 Client"), "plural": _("OAuth2 Clients")}
|
|
393
|
+
__c2cgeoform_config__ = {"duplicate": True}
|
|
394
|
+
id: Mapped[int] = mapped_column(
|
|
395
|
+
Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
|
|
396
|
+
)
|
|
397
|
+
client_id: Mapped[str] = mapped_column(
|
|
398
|
+
Unicode,
|
|
399
|
+
unique=True,
|
|
400
|
+
info={
|
|
401
|
+
"colanderalchemy": {
|
|
402
|
+
"title": _("Client ID"),
|
|
403
|
+
"description": _("The client identifier as e.-g. 'qgis'."),
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
)
|
|
407
|
+
secret: Mapped[str] = mapped_column(
|
|
408
|
+
Unicode,
|
|
409
|
+
info={
|
|
410
|
+
"colanderalchemy": {
|
|
411
|
+
"title": _("Secret"),
|
|
412
|
+
"description": _("The secret."),
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
)
|
|
416
|
+
redirect_uri: Mapped[str] = mapped_column(
|
|
417
|
+
Unicode,
|
|
418
|
+
info={
|
|
419
|
+
"colanderalchemy": {
|
|
420
|
+
"title": _("Redirect URI"),
|
|
421
|
+
"description": _(
|
|
422
|
+
"""
|
|
423
|
+
URI where user should be redirected after authentication
|
|
424
|
+
as e.-g. 'http://127.0.0.1:7070/' in case of QGIS desktop.
|
|
425
|
+
"""
|
|
426
|
+
),
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
state_required: Mapped[bool] = mapped_column(
|
|
431
|
+
Boolean,
|
|
432
|
+
default=False,
|
|
433
|
+
info={
|
|
434
|
+
"colanderalchemy": {
|
|
435
|
+
"title": _("State required"),
|
|
436
|
+
"description": _(
|
|
437
|
+
"The state is required for this client (see: "
|
|
438
|
+
"https://auth0.com/docs/secure/attack-protection/state-parameters)."
|
|
439
|
+
),
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
pkce_required: Mapped[bool] = mapped_column(
|
|
444
|
+
Boolean,
|
|
445
|
+
default=False,
|
|
446
|
+
info={
|
|
447
|
+
"colanderalchemy": {
|
|
448
|
+
"title": _("PKCE required"),
|
|
449
|
+
"description": _(
|
|
450
|
+
"PKCE is required for this client (see: "
|
|
451
|
+
"https://auth0.com/docs/get-started/authentication-and-authorization-flow/"
|
|
452
|
+
"authorization-code-flow-with-proof-key-for-code-exchange-pkce)."
|
|
453
|
+
),
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class OAuth2BearerToken(Base): # type: ignore
|
|
460
|
+
"""The oauth2_bearertoken table representation."""
|
|
461
|
+
|
|
462
|
+
__tablename__ = "oauth2_bearertoken"
|
|
463
|
+
__table_args__ = (
|
|
464
|
+
sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
|
|
465
|
+
{
|
|
466
|
+
"schema": _schema,
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
470
|
+
client_id: Mapped[int] = mapped_column(
|
|
471
|
+
Integer, ForeignKey(_schema + ".oauth2_client.id", ondelete="CASCADE"), nullable=False
|
|
472
|
+
)
|
|
473
|
+
client = relationship(OAuth2Client)
|
|
474
|
+
user_id: Mapped[int] = mapped_column(
|
|
475
|
+
Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
|
|
476
|
+
)
|
|
477
|
+
user = relationship(User)
|
|
478
|
+
access_token: Mapped[str] = mapped_column(Unicode(100), unique=True)
|
|
479
|
+
refresh_token: Mapped[str] = mapped_column(Unicode(100), unique=True)
|
|
480
|
+
expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) # in one hour
|
|
481
|
+
state = mapped_column(String, nullable=True)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class OAuth2AuthorizationCode(Base): # type: ignore
|
|
485
|
+
"""The oauth2_authorizationcode table representation."""
|
|
486
|
+
|
|
487
|
+
__tablename__ = "oauth2_authorizationcode"
|
|
488
|
+
__table_args__ = (
|
|
489
|
+
sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
|
|
490
|
+
{
|
|
491
|
+
"schema": _schema,
|
|
492
|
+
},
|
|
493
|
+
)
|
|
494
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
495
|
+
client_id: Mapped[int] = mapped_column(
|
|
496
|
+
Integer, ForeignKey(_schema + ".oauth2_client.id", ondelete="CASCADE"), nullable=False
|
|
497
|
+
)
|
|
498
|
+
client = relationship(OAuth2Client)
|
|
499
|
+
user_id: Mapped[int] = mapped_column(
|
|
500
|
+
Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
|
|
501
|
+
)
|
|
502
|
+
user = relationship(User)
|
|
503
|
+
redirect_uri: Mapped[str] = mapped_column(Unicode)
|
|
504
|
+
code: Mapped[str] = mapped_column(Unicode(100), unique=True, nullable=True)
|
|
505
|
+
state: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
506
|
+
challenge: Mapped[str] = mapped_column(String(128), nullable=True)
|
|
507
|
+
challenge_method: Mapped[str] = mapped_column(String(6), nullable=True)
|
|
508
|
+
expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) # in 10 minutes
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
class Log(AbstractLog):
|
|
512
|
+
"""The static log table representation."""
|
|
513
|
+
|
|
514
|
+
__tablename__ = "log"
|
|
515
|
+
__table_args__ = {"schema": _schema}
|
|
516
|
+
__mapper_args__ = {
|
|
517
|
+
"polymorphic_identity": "static",
|
|
518
|
+
"concrete": True,
|
|
519
|
+
}
|
|
File without changes
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2018-2020, Camptocamp SA
|
|
1
|
+
# Copyright (c) 2018-2024, Camptocamp SA
|
|
4
2
|
# All rights reserved.
|
|
5
3
|
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -28,24 +26,31 @@
|
|
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
|
29
27
|
|
|
30
28
|
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import zope.sqlalchemy
|
|
31
32
|
from sqlalchemy import engine_from_config
|
|
32
33
|
from sqlalchemy.engine import Engine
|
|
33
34
|
from sqlalchemy.orm import Session, configure_mappers, sessionmaker
|
|
34
35
|
from transaction import TransactionManager
|
|
35
|
-
import zope.sqlalchemy
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def get_engine(settings: dict, prefix: str = "sqlalchemy.") -> Engine:
|
|
38
|
+
def get_engine(settings: dict[str, Any], prefix: str = "sqlalchemy.") -> Engine:
|
|
39
|
+
"""Get the engine."""
|
|
39
40
|
return engine_from_config(settings, prefix)
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
def get_session_factory(engine: Engine) -> sessionmaker:
|
|
43
|
+
def get_session_factory(engine: Engine) -> sessionmaker[Session]: # pylint: disable=unsubscriptable-object
|
|
44
|
+
"""Get the session factory."""
|
|
43
45
|
factory = sessionmaker()
|
|
44
46
|
factory.configure(bind=engine)
|
|
45
47
|
return factory
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
def get_tm_session(
|
|
50
|
+
def get_tm_session(
|
|
51
|
+
session_factory: sessionmaker[Session], # pylint: disable=unsubscriptable-object
|
|
52
|
+
transaction_manager: TransactionManager,
|
|
53
|
+
) -> Session:
|
|
49
54
|
"""
|
|
50
55
|
Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
|
|
51
56
|
|
|
@@ -64,21 +69,20 @@ def get_tm_session(session_factory: sessionmaker, transaction_manager: Transacti
|
|
|
64
69
|
session_factory = get_session_factory(engine)
|
|
65
70
|
with transaction.manager:
|
|
66
71
|
dbsession = get_tm_session(session_factory, transaction.manager)
|
|
67
|
-
|
|
68
72
|
"""
|
|
69
73
|
dbsession = session_factory()
|
|
74
|
+
assert isinstance(dbsession, Session)
|
|
70
75
|
zope.sqlalchemy.register(dbsession, transaction_manager=transaction_manager)
|
|
71
76
|
return dbsession
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
def generate_mappers() -> None:
|
|
75
|
-
"""
|
|
76
|
-
Initialize the model for a Pyramid app.
|
|
77
|
-
"""
|
|
80
|
+
"""Initialize the model for a Pyramid app."""
|
|
78
81
|
|
|
79
82
|
# import or define all models here to ensure they are attached to the
|
|
80
83
|
# Base.metadata prior to any initialization routines
|
|
81
|
-
import c2cgeoportal_commons.models.main #
|
|
84
|
+
import c2cgeoportal_commons.models.main # pylint: disable=unused-import,import-outside-toplevel
|
|
85
|
+
import c2cgeoportal_commons.models.static # pylint: disable=import-outside-toplevel
|
|
82
86
|
|
|
83
87
|
# run configure_mappers after defining all of the models to ensure
|
|
84
88
|
# all relationships can be setup
|
|
@@ -86,8 +90,9 @@ def generate_mappers() -> None:
|
|
|
86
90
|
|
|
87
91
|
|
|
88
92
|
def get_session(
|
|
89
|
-
settings: dict, transaction_manager: TransactionManager, prefix: str = "sqlalchemy."
|
|
93
|
+
settings: dict[str, Any], transaction_manager: TransactionManager, prefix: str = "sqlalchemy."
|
|
90
94
|
) -> Session:
|
|
95
|
+
"""Get the session."""
|
|
91
96
|
configure_mappers()
|
|
92
97
|
engine = get_engine(settings, prefix)
|
|
93
98
|
session_factory = get_session_factory(engine)
|