c2cgeoportal-commons 2.6.0__py3-none-any.whl → 2.9rc45__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 +49 -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 +21 -24
- 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 +9 -6
- 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 +11 -8
- 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 +15 -12
- 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 +9 -6
- 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 +9 -6
- 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 +14 -16
- c2cgeoportal_commons/lib/literal.py +44 -0
- c2cgeoportal_commons/lib/url.py +164 -58
- c2cgeoportal_commons/lib/validators.py +2 -4
- c2cgeoportal_commons/models/__init__.py +19 -15
- c2cgeoportal_commons/models/main.py +1191 -263
- c2cgeoportal_commons/models/sqlalchemy.py +14 -18
- c2cgeoportal_commons/models/static.py +305 -78
- c2cgeoportal_commons/py.typed +0 -0
- c2cgeoportal_commons/testing/__init__.py +17 -12
- c2cgeoportal_commons/testing/initializedb.py +15 -14
- {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/METADATA +15 -16
- c2cgeoportal_commons-2.9rc45.dist-info/RECORD +89 -0
- {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/WHEEL +1 -1
- tests/conftest.py +1 -3
- c2cgeoportal_commons-2.6.0.dist-info/RECORD +0 -76
- {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2012-2021, 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):
|
|
57
|
+
class TsVector(UserDefinedType[dict[str, str]]): # pylint: disable=abstract-method
|
|
64
58
|
"""A custom type for PostgreSQL's tsvector type."""
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
cache_ok = True
|
|
61
|
+
|
|
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-2021, 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
|
|
@@ -30,31 +28,34 @@
|
|
|
30
28
|
|
|
31
29
|
import crypt
|
|
32
30
|
import logging
|
|
33
|
-
|
|
31
|
+
import os
|
|
32
|
+
from datetime import datetime, timezone
|
|
34
33
|
from hashlib import sha1
|
|
35
34
|
from hmac import compare_digest as compare_hash
|
|
36
|
-
from typing import Any
|
|
35
|
+
from typing import Any
|
|
37
36
|
|
|
38
|
-
import pytz
|
|
39
37
|
import sqlalchemy.schema
|
|
40
38
|
from c2c.template.config import config
|
|
41
39
|
from sqlalchemy import Column, ForeignKey, Table
|
|
42
40
|
from sqlalchemy.dialects.postgresql import HSTORE
|
|
43
41
|
from sqlalchemy.ext.mutable import MutableDict
|
|
44
|
-
from sqlalchemy.orm import backref, relationship
|
|
42
|
+
from sqlalchemy.orm import Mapped, backref, mapped_column, relationship
|
|
45
43
|
from sqlalchemy.types import Boolean, DateTime, Integer, String, Unicode
|
|
46
44
|
|
|
45
|
+
from c2cgeoportal_commons.lib.literal import Literal
|
|
47
46
|
from c2cgeoportal_commons.models import Base, _
|
|
48
|
-
from c2cgeoportal_commons.models.main import Role
|
|
47
|
+
from c2cgeoportal_commons.models.main import AbstractLog, Role
|
|
49
48
|
|
|
50
49
|
try:
|
|
51
50
|
from c2cgeoform.ext.deform_ext import RelationSelect2Widget
|
|
52
51
|
from colander import Email, drop
|
|
53
52
|
from deform.widget import DateTimeInputWidget, HiddenWidget
|
|
54
53
|
except ModuleNotFoundError:
|
|
55
|
-
drop = None
|
|
54
|
+
drop = None # pylint: disable=invalid-name
|
|
56
55
|
|
|
57
56
|
class GenericClass:
|
|
57
|
+
"""Generic class."""
|
|
58
|
+
|
|
58
59
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
59
60
|
pass
|
|
60
61
|
|
|
@@ -62,10 +63,11 @@ except ModuleNotFoundError:
|
|
|
62
63
|
HiddenWidget = GenericClass
|
|
63
64
|
DateTimeInputWidget = GenericClass
|
|
64
65
|
CollenderGeometry = GenericClass
|
|
65
|
-
RelationSelect2Widget = GenericClass
|
|
66
|
+
RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
_LOG = logging.getLogger(__name__)
|
|
70
|
+
_OPENID_CONNECT_ENABLED = os.environ.get("OPENID_CONNECT_ENABLED", "false").lower() in ("true", "yes", "1")
|
|
69
71
|
|
|
70
72
|
_schema: str = config["schema_static"] or "static"
|
|
71
73
|
|
|
@@ -79,38 +81,124 @@ user_role = Table(
|
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
|
|
82
|
-
class User(Base):
|
|
84
|
+
class User(Base): # type: ignore
|
|
85
|
+
"""The user table representation."""
|
|
86
|
+
|
|
83
87
|
__tablename__ = "user"
|
|
84
88
|
__table_args__ = {"schema": _schema}
|
|
85
|
-
__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
|
+
}
|
|
86
126
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
87
|
-
item_type =
|
|
127
|
+
item_type: Mapped[str] = mapped_column(
|
|
88
128
|
"type", String(10), nullable=False, info={"colanderalchemy": {"widget": HiddenWidget()}}
|
|
89
129
|
)
|
|
90
130
|
__mapper_args__ = {"polymorphic_on": item_type, "polymorphic_identity": "user"}
|
|
91
131
|
|
|
92
|
-
id
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
},
|
|
95
149
|
)
|
|
96
|
-
|
|
97
|
-
|
|
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}}
|
|
161
|
+
)
|
|
162
|
+
temp_password: Mapped[str | None] = mapped_column(
|
|
98
163
|
"temp_password", Unicode, nullable=True, info={"colanderalchemy": {"exclude": True}}
|
|
99
164
|
)
|
|
100
|
-
tech_data =
|
|
101
|
-
email =
|
|
102
|
-
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
|
+
},
|
|
103
179
|
)
|
|
104
|
-
is_password_changed =
|
|
105
|
-
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
|
+
},
|
|
106
193
|
)
|
|
107
194
|
|
|
108
|
-
settings_role_id =
|
|
109
|
-
Integer,
|
|
195
|
+
settings_role_id: Mapped[int] = mapped_column(
|
|
196
|
+
Integer(),
|
|
197
|
+
nullable=True,
|
|
110
198
|
info={
|
|
111
199
|
"colanderalchemy": {
|
|
112
200
|
"title": _("Settings from role"),
|
|
113
|
-
"description": "
|
|
201
|
+
"description": _("Used to get some settings for the user (not for permissions)."),
|
|
114
202
|
"widget": RelationSelect2Widget(
|
|
115
203
|
Role, "id", "name", order_by="name", default_value=("", _("- Select -"))
|
|
116
204
|
),
|
|
@@ -129,37 +217,86 @@ class User(Base):
|
|
|
129
217
|
Role,
|
|
130
218
|
secondary=user_role,
|
|
131
219
|
secondaryjoin=Role.id == user_role.c.role_id,
|
|
132
|
-
backref=backref(
|
|
133
|
-
|
|
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
|
+
},
|
|
134
238
|
)
|
|
135
239
|
|
|
136
|
-
last_login =
|
|
240
|
+
last_login: Mapped[datetime] = mapped_column(
|
|
137
241
|
DateTime(timezone=True),
|
|
242
|
+
nullable=True,
|
|
138
243
|
info={
|
|
139
|
-
"colanderalchemy":
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
)
|
|
144
254
|
},
|
|
145
255
|
)
|
|
146
256
|
|
|
147
|
-
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
|
+
)
|
|
148
270
|
|
|
149
|
-
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
|
+
)
|
|
150
285
|
|
|
151
|
-
def __init__(
|
|
286
|
+
def __init__( # nosec
|
|
152
287
|
self,
|
|
153
288
|
username: str = "",
|
|
154
289
|
password: str = "",
|
|
155
290
|
email: str = "",
|
|
156
291
|
is_password_changed: bool = False,
|
|
157
|
-
settings_role: Role = None,
|
|
158
|
-
roles:
|
|
159
|
-
expire_on: datetime = None,
|
|
292
|
+
settings_role: Role | None = None,
|
|
293
|
+
roles: list[Role] | None = None,
|
|
294
|
+
expire_on: datetime | None = None,
|
|
160
295
|
deactivated: bool = False,
|
|
296
|
+
display_name: str | None = None,
|
|
161
297
|
) -> None:
|
|
162
298
|
self.username = username
|
|
299
|
+
self.display_name = display_name or username
|
|
163
300
|
self.password = password
|
|
164
301
|
self.tech_data = {}
|
|
165
302
|
self.email = email
|
|
@@ -172,16 +309,16 @@ class User(Base):
|
|
|
172
309
|
|
|
173
310
|
@property
|
|
174
311
|
def password(self) -> str:
|
|
175
|
-
"""
|
|
176
|
-
return self._password
|
|
312
|
+
"""Get the password."""
|
|
313
|
+
return self._password
|
|
177
314
|
|
|
178
315
|
@password.setter
|
|
179
316
|
def password(self, password: str) -> None:
|
|
180
|
-
"""
|
|
317
|
+
"""Encrypt password on the fly."""
|
|
181
318
|
self._password = self.__encrypt_password(password)
|
|
182
319
|
|
|
183
320
|
def set_temp_password(self, password: str) -> None:
|
|
184
|
-
"""
|
|
321
|
+
"""Encrypt password on the fly."""
|
|
185
322
|
self.temp_password = self.__encrypt_password(password)
|
|
186
323
|
|
|
187
324
|
@staticmethod
|
|
@@ -194,8 +331,8 @@ class User(Base):
|
|
|
194
331
|
return crypt.crypt(password, crypt.METHOD_SHA512)
|
|
195
332
|
|
|
196
333
|
def validate_password(self, passwd: str) -> bool:
|
|
197
|
-
"""
|
|
198
|
-
this method _MUST_ return a boolean.
|
|
334
|
+
"""
|
|
335
|
+
Check the password against existing credentials. this method _MUST_ return a boolean.
|
|
199
336
|
|
|
200
337
|
@param passwd: the password that was provided by the user to
|
|
201
338
|
try and authenticate. This is the clear text version that we will
|
|
@@ -214,7 +351,7 @@ class User(Base):
|
|
|
214
351
|
|
|
215
352
|
if (
|
|
216
353
|
self.temp_password is not None
|
|
217
|
-
and self.temp_password != ""
|
|
354
|
+
and self.temp_password != "" # nosec
|
|
218
355
|
and compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password))
|
|
219
356
|
):
|
|
220
357
|
self._password = self.temp_password
|
|
@@ -224,39 +361,104 @@ class User(Base):
|
|
|
224
361
|
return False
|
|
225
362
|
|
|
226
363
|
def expired(self) -> bool:
|
|
227
|
-
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)
|
|
228
365
|
|
|
229
366
|
def update_last_login(self) -> None:
|
|
230
|
-
self.last_login = datetime.now(
|
|
367
|
+
self.last_login = datetime.now(timezone.utc)
|
|
231
368
|
|
|
232
369
|
def __str__(self) -> str:
|
|
233
|
-
return self.username or ""
|
|
370
|
+
return self.username or ""
|
|
234
371
|
|
|
235
372
|
|
|
236
|
-
class Shorturl(Base):
|
|
373
|
+
class Shorturl(Base): # type: ignore
|
|
374
|
+
"""The shorturl table representation."""
|
|
375
|
+
|
|
237
376
|
__tablename__ = "shorturl"
|
|
238
377
|
__table_args__ = {"schema": _schema}
|
|
239
|
-
id =
|
|
240
|
-
url =
|
|
241
|
-
ref =
|
|
242
|
-
creator_email =
|
|
243
|
-
creation =
|
|
244
|
-
last_hit =
|
|
245
|
-
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
|
+
|
|
246
386
|
|
|
387
|
+
class OAuth2Client(Base): # type: ignore
|
|
388
|
+
"""The oauth2_client table representation."""
|
|
247
389
|
|
|
248
|
-
class OAuth2Client(Base):
|
|
249
390
|
__tablename__ = "oauth2_client"
|
|
250
391
|
__table_args__ = {"schema": _schema}
|
|
251
392
|
__colanderalchemy_config__ = {"title": _("OAuth2 Client"), "plural": _("OAuth2 Clients")}
|
|
252
393
|
__c2cgeoform_config__ = {"duplicate": True}
|
|
253
|
-
id
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
|
|
257
458
|
|
|
459
|
+
class OAuth2BearerToken(Base): # type: ignore
|
|
460
|
+
"""The oauth2_bearertoken table representation."""
|
|
258
461
|
|
|
259
|
-
class OAuth2BearerToken(Base):
|
|
260
462
|
__tablename__ = "oauth2_bearertoken"
|
|
261
463
|
__table_args__ = (
|
|
262
464
|
sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
|
|
@@ -264,17 +466,24 @@ class OAuth2BearerToken(Base):
|
|
|
264
466
|
"schema": _schema,
|
|
265
467
|
},
|
|
266
468
|
)
|
|
267
|
-
id =
|
|
268
|
-
client_id =
|
|
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
|
+
)
|
|
269
473
|
client = relationship(OAuth2Client)
|
|
270
|
-
user_id =
|
|
474
|
+
user_id: Mapped[int] = mapped_column(
|
|
475
|
+
Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
|
|
476
|
+
)
|
|
271
477
|
user = relationship(User)
|
|
272
|
-
access_token =
|
|
273
|
-
refresh_token =
|
|
274
|
-
expire_at =
|
|
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
|
+
|
|
275
483
|
|
|
484
|
+
class OAuth2AuthorizationCode(Base): # type: ignore
|
|
485
|
+
"""The oauth2_authorizationcode table representation."""
|
|
276
486
|
|
|
277
|
-
class OAuth2AuthorizationCode(Base):
|
|
278
487
|
__tablename__ = "oauth2_authorizationcode"
|
|
279
488
|
__table_args__ = (
|
|
280
489
|
sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
|
|
@@ -282,11 +491,29 @@ class OAuth2AuthorizationCode(Base):
|
|
|
282
491
|
"schema": _schema,
|
|
283
492
|
},
|
|
284
493
|
)
|
|
285
|
-
id =
|
|
286
|
-
client_id =
|
|
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
|
+
)
|
|
287
498
|
client = relationship(OAuth2Client)
|
|
288
|
-
user_id =
|
|
499
|
+
user_id: Mapped[int] = mapped_column(
|
|
500
|
+
Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
|
|
501
|
+
)
|
|
289
502
|
user = relationship(User)
|
|
290
|
-
redirect_uri =
|
|
291
|
-
code =
|
|
292
|
-
|
|
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
|