ecodev-core 0.0.52__py3-none-any.whl → 0.0.54__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 ecodev-core might be problematic. Click here for more details.

ecodev_core/__init__.py CHANGED
@@ -12,6 +12,7 @@ from ecodev_core.app_user import select_user
12
12
  from ecodev_core.app_user import upsert_app_users
13
13
  from ecodev_core.auth_configuration import AUTH
14
14
  from ecodev_core.authentication import attempt_to_log
15
+ from ecodev_core.authentication import ban_token
15
16
  from ecodev_core.authentication import get_access_token
16
17
  from ecodev_core.authentication import get_app_services
17
18
  from ecodev_core.authentication import get_current_user
@@ -40,6 +41,7 @@ from ecodev_core.db_insertion import get_raw_df
40
41
  from ecodev_core.db_retrieval import count_rows
41
42
  from ecodev_core.db_retrieval import get_rows
42
43
  from ecodev_core.db_retrieval import ServerSideField
44
+ from ecodev_core.db_upsertion import add_missing_enum_values
43
45
  from ecodev_core.db_upsertion import field
44
46
  from ecodev_core.db_upsertion import filter_to_sfield_dict
45
47
  from ecodev_core.db_upsertion import get_sfield_columns
@@ -87,6 +89,7 @@ from ecodev_core.safe_utils import SimpleReturn
87
89
  from ecodev_core.safe_utils import stringify
88
90
  from ecodev_core.settings import SETTINGS
89
91
  from ecodev_core.settings import Settings
92
+ from ecodev_core.token_banlist import TokenBanlist
90
93
  from ecodev_core.version import db_to_value
91
94
  from ecodev_core.version import get_row_versions
92
95
  from ecodev_core.version import get_versions
@@ -107,4 +110,4 @@ __all__ = [
107
110
  'sort_by_keys', 'sort_by_values', 'Settings', 'load_yaml_file', 'Deployment', 'Version',
108
111
  'sfield', 'field', 'upsert_df_data', 'upsert_deletor', 'get_row_versions', 'get_versions',
109
112
  'db_to_value', 'upsert_data', 'upsert_selector', 'get_sfield_columns', 'filter_to_sfield_dict',
110
- 'SETTINGS']
113
+ 'SETTINGS', 'add_missing_enum_values', 'ban_token', 'TokenBanlist']
@@ -33,7 +33,7 @@ from ecodev_core.db_connection import engine
33
33
  from ecodev_core.logger import logger_get
34
34
  from ecodev_core.permissions import Permission
35
35
  from ecodev_core.pydantic_utils import Frozen
36
-
36
+ from ecodev_core.token_banlist import TokenBanlist
37
37
 
38
38
  SCHEME = OAuth2PasswordBearer(tokenUrl='login')
39
39
  auth_router = APIRouter(tags=['authentication'])
@@ -44,6 +44,7 @@ INVALID_USER = 'Invalid User'
44
44
  INVALID_TFA = 'Invalid TFA code'
45
45
  ADMIN_ERROR = 'Could not validate credentials. You need admin rights to call this'
46
46
  INVALID_CREDENTIALS = 'Invalid Credentials'
47
+ REVOKED_TOKEN = 'This token has been revoked (by a logout action), please login again.'
47
48
  log = logger_get(__name__)
48
49
 
49
50
 
@@ -62,7 +63,7 @@ class TokenData(Frozen):
62
63
  id: int
63
64
 
64
65
 
65
- def get_access_token(token: Dict[str, Any]):
66
+ def get_access_token(token: Dict[str, Any]) -> str | None:
66
67
  """
67
68
  Robust method to return access token or None
68
69
  """
@@ -158,6 +159,9 @@ def is_authorized_user(token: str = Depends(SCHEME)) -> bool:
158
159
  """
159
160
  Check if the passed token corresponds to an authorized user
160
161
  """
162
+ if _is_banned(token):
163
+ return False
164
+
161
165
  try:
162
166
  return get_current_user(token) is not None
163
167
  except Exception:
@@ -180,12 +184,38 @@ def get_user(token: str = Depends(SCHEME),
180
184
  """
181
185
  Retrieves (if it exists) the db user corresponding to the passed token
182
186
  """
187
+ if _is_banned(token):
188
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=REVOKED_TOKEN,
189
+ headers={'WWW-Authenticate': 'Bearer'})
183
190
  if user := get_current_user(token, tfa_value, tfa_check):
184
191
  return user
185
192
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_CREDENTIALS,
186
193
  headers={'WWW-Authenticate': 'Bearer'})
187
194
 
188
195
 
196
+ def ban_token(token: str, session: Session) -> None:
197
+ """
198
+ Ban the passed token
199
+ """
200
+ session.add(TokenBanlist(token=token))
201
+ session.commit()
202
+
203
+
204
+ def _is_banned(token: str) -> bool:
205
+ """
206
+ Check if the passed token is banned.
207
+
208
+ NB: Clean the TokenBanlist table (deleting old entries) on the fly
209
+ """
210
+ with Session(engine) as session:
211
+ threshold = datetime.now() - timedelta(minutes=EXPIRATION_LENGTH)
212
+ for token_banned in session.exec(
213
+ select(TokenBanlist).where(TokenBanlist.created_at <= threshold)).all():
214
+ session.delete(token_banned)
215
+ session.commit()
216
+ return token in session.exec(select(TokenBanlist.token)).all()
217
+
218
+
189
219
  def get_current_user(token: str,
190
220
  tfa_value: Optional[str] = None,
191
221
  tfa_check: bool = False
@@ -202,6 +232,10 @@ def is_admin_user(token: str = Depends(SCHEME)) -> AppUser:
202
232
  """
203
233
  Retrieves (if it exists) the admin (meaning who has valid credentials) user from the db
204
234
  """
235
+ if _is_banned(token):
236
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=REVOKED_TOKEN,
237
+ headers={'WWW-Authenticate': 'Bearer'})
238
+
205
239
  if (user := get_current_user(token)) and user.permission == Permission.ADMIN:
206
240
  return user
207
241
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ADMIN_ERROR,
@@ -212,6 +246,10 @@ def is_monitoring_user(token: str = Depends(SCHEME)) -> AppUser:
212
246
  """
213
247
  Retrieves (if it exists) the monitoring user from the db
214
248
  """
249
+ if _is_banned(token):
250
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=REVOKED_TOKEN,
251
+ headers={'WWW-Authenticate': 'Bearer'})
252
+
215
253
  if (user := get_current_user(token)) and user.user == MONITORING:
216
254
  return user
217
255
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
@@ -2,6 +2,7 @@
2
2
  Module handling CRUD and version operations
3
3
  """
4
4
  from datetime import datetime
5
+ from enum import EnumType
5
6
  from functools import partial
6
7
  from typing import Any
7
8
  from typing import Union
@@ -14,6 +15,7 @@ from sqlmodel import inspect
14
15
  from sqlmodel import select
15
16
  from sqlmodel import Session
16
17
  from sqlmodel import SQLModel
18
+ from sqlmodel import text
17
19
  from sqlmodel import update
18
20
  from sqlmodel.main import SQLModelMetaclass
19
21
  from sqlmodel.sql.expression import SelectOfScalar
@@ -27,6 +29,33 @@ INFO = 'info'
27
29
  SA_COLUMN_KWARGS = 'sa_column_kwargs'
28
30
 
29
31
 
32
+ def add_missing_enum_values(enum: EnumType, session: Session, new_vals: list | None = None) -> None:
33
+ """
34
+ Add to an existing enum its missing db values. Do so by retrieving what is already in db, and
35
+ insert what is new.
36
+
37
+ NB: new_val argument is there for testing purposes
38
+ """
39
+
40
+ for val in [e.name for e in new_vals or enum if e.name not in get_enum_values(enum, session)]:
41
+ session.execute(text(f"ALTER TYPE {enum.__name__.lower()} ADD VALUE IF NOT EXISTS '{val}'"))
42
+ session.commit()
43
+
44
+
45
+ def get_enum_values(enum: EnumType, session: Session) -> set[str]:
46
+ """
47
+ Return all enum values in db for the passed enum.
48
+ """
49
+ result = session.execute(text(
50
+ """
51
+ SELECT enumlabel FROM pg_enum
52
+ JOIN pg_type ON pg_enum.enumtypid = pg_type.oid
53
+ WHERE pg_type.typname = :enum_name
54
+ """
55
+ ), {'enum_name': enum.__name__.lower()})
56
+ return {x[0] for x in result}
57
+
58
+
30
59
  def sfield(**kwargs):
31
60
  """
32
61
  Field constructor for columns not to be versioned. Those are the columns on which to select.
@@ -147,11 +176,11 @@ def get_sfield_columns(db_model: SQLModelMetaclass) -> list[str]:
147
176
  for x in inspect(db_model).c
148
177
  if x.info.get(FILTER_ON) is True
149
178
  ]
150
-
151
-
152
- def filter_to_sfield_dict(row: dict | SQLModelMetaclass,
153
- db_schema: SQLModelMetaclass | None = None) \
154
- -> dict[str, dict | SQLModelMetaclass]:
179
+
180
+
181
+ def filter_to_sfield_dict(row: dict | SQLModelMetaclass,
182
+ db_schema: SQLModelMetaclass | None = None) \
183
+ -> dict[str, dict | SQLModelMetaclass]:
155
184
  """
156
185
  Returns a dict with only sfields from object
157
186
  Args:
@@ -162,4 +191,3 @@ def filter_to_sfield_dict(row: dict | SQLModelMetaclass,
162
191
  """
163
192
  return {pk: getattr(row, pk)
164
193
  for pk in get_sfield_columns(db_schema or row.__class__)}
165
-
@@ -0,0 +1,18 @@
1
+ """
2
+ Module implementing the token ban list table
3
+ """
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+ from sqlmodel import Field
8
+ from sqlmodel import SQLModel
9
+
10
+
11
+ class TokenBanlist(SQLModel, table=True): # type: ignore
12
+ """
13
+ A token banlist: timestamped banned token.
14
+ """
15
+ __tablename__ = 'token_banlist'
16
+ id: Optional[int] = Field(default=None, primary_key=True)
17
+ created_at: datetime = Field(default_factory=datetime.utcnow)
18
+ token: str = Field(index=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ecodev-core
3
- Version: 0.0.52
3
+ Version: 0.0.54
4
4
  Summary: Low level sqlmodel/fastapi/pydantic building blocks
5
5
  License: MIT
6
6
  Author: Thomas Epelbaum
@@ -43,7 +43,7 @@ Requires-Dist: pandas (>=2,<3)
43
43
  Requires-Dist: passlib[bcyrypt] (>=1,<2)
44
44
  Requires-Dist: progressbar2 (>=4.4.2,<5.0.0)
45
45
  Requires-Dist: psycopg2-binary (>=2,<3)
46
- Requires-Dist: pydantic (==2.9.2)
46
+ Requires-Dist: pydantic (==2.11.7)
47
47
  Requires-Dist: pydantic-settings (>=2,<3)
48
48
  Requires-Dist: python-jose[cryptography] (>=3,<4)
49
49
  Requires-Dist: pyyaml (>=6,<7)
@@ -1,9 +1,9 @@
1
- ecodev_core/__init__.py,sha256=ULUNsG-OwP5pZkqFemYcA5NZURCdSjrC2v4PNTlreDs,6001
1
+ ecodev_core/__init__.py,sha256=xS1h7Xtp-LBGeKkxFfJFqyxeBiznow7c1MPNAeYLtOg,6218
2
2
  ecodev_core/app_activity.py,sha256=KBtI-35LBLPDppFB7xjxWthXQrY3Z_aGDnC-HrW8Ea0,4641
3
3
  ecodev_core/app_rights.py,sha256=RZPdDtydFqc_nFj96huKAc56BS0qS6ScKv4Kghqd6lc,726
4
4
  ecodev_core/app_user.py,sha256=r1bqA4H08x53XmxmjwyGKl_PFjYQazzBbVErdkztqeE,2947
5
5
  ecodev_core/auth_configuration.py,sha256=qZ1Dkk7n1AH7w0tVKQ8AYswukOeMZH6mmbixPEAQnJ8,764
6
- ecodev_core/authentication.py,sha256=HYi4C7cHFwGUoskiMK2q-X7QeGofONPYWB55kxzJ6vI,10093
6
+ ecodev_core/authentication.py,sha256=HuA8o_A_jjnTzG6NKqIeh5LP1tb0yyCR1RvXcPy2Xks,11559
7
7
  ecodev_core/backup.py,sha256=N5AtoqtHJRp92Bj0Nr7WW5WDcpjTIET8haxZoYDOtyI,3890
8
8
  ecodev_core/check_dependencies.py,sha256=aFn8GI4eBbuJT8RxsfhSSnlpNYYj_LPOH-tZF0EqfKQ,6917
9
9
  ecodev_core/custom_equal.py,sha256=2gRn0qpyJ8-Kw9GQSueu0nLngLrRrwyMPlP6zqPac0U,899
@@ -11,7 +11,7 @@ ecodev_core/db_connection.py,sha256=hhqeyTrl0DlQA7RkUs6pIOpZeE3yS_Q5mqj5uGPfG_Y,
11
11
  ecodev_core/db_filters.py,sha256=T_5JVF27UEu7sC6NOm7-W3_Y0GLfbWQO_EeTXcD2cv8,5041
12
12
  ecodev_core/db_insertion.py,sha256=k-r798MMrW1sRb-gb8lQTxyJrb4QP5iZT8GDzCYYwlo,4544
13
13
  ecodev_core/db_retrieval.py,sha256=sCP7TDGIcTOK5gT3Inga91bE4S31HbQPw4yI22WJbss,7392
14
- ecodev_core/db_upsertion.py,sha256=_OdCILP1NEw1hkbvTn9ZOF5YkU9U02Fj_U3E7uY5AoI,5861
14
+ ecodev_core/db_upsertion.py,sha256=Nri5innUEcUBc5zFWDq9oqyzC5sjkX7xQHwqriZ6OUI,6814
15
15
  ecodev_core/deployment.py,sha256=z8ACI00EtKknXOB8xyPwYIXTvPjIDOH9z9cBGEU0YrA,281
16
16
  ecodev_core/email_sender.py,sha256=V3UGweuq6Iy09Z9to8HzM6JOVDVGHZXHGjUSkW94Tac,1912
17
17
  ecodev_core/enum_utils.py,sha256=BkQ4YQ97tXBYmMcQiSIi0mbioD5CgVU79myg1BBAXuA,556
@@ -25,8 +25,9 @@ ecodev_core/read_write.py,sha256=YIGRERvFHU7vy-JIaCWAza4CPMysLRUHKJxN-ZgFmu0,120
25
25
  ecodev_core/safe_utils.py,sha256=Q8N15El1tSxZJJsy1i_1CCycuBN1_98QQoHmYJRcLIY,6904
26
26
  ecodev_core/settings.py,sha256=UvaTv8S_HvfFAL-m1Rfqv_geSGcccuV3ziR1o1d5wu4,1795
27
27
  ecodev_core/sqlmodel_utils.py,sha256=t57H3QPtKRy4ujic1clMK_2L4p0yjGJLZbDjHPZ8M94,453
28
+ ecodev_core/token_banlist.py,sha256=rKXG9QkfCpjOTr8gBgdX-KYNHAkKvQ9TRnGS99VC9Co,491
28
29
  ecodev_core/version.py,sha256=eyIf8KkW_t-hMuYFIoy0cUlNaMewLe6i45m2HKZKh0Q,4403
29
- ecodev_core-0.0.52.dist-info/LICENSE.md,sha256=8dqVJEbwXjPWjjRKjdLMym5k9Gi8hwtrHh84sti6KIs,1068
30
- ecodev_core-0.0.52.dist-info/METADATA,sha256=UgEUyk-1FHtzUXSP3t7fattxjRHbSVXjwvMI8_Ioa8g,3509
31
- ecodev_core-0.0.52.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- ecodev_core-0.0.52.dist-info/RECORD,,
30
+ ecodev_core-0.0.54.dist-info/LICENSE.md,sha256=8dqVJEbwXjPWjjRKjdLMym5k9Gi8hwtrHh84sti6KIs,1068
31
+ ecodev_core-0.0.54.dist-info/METADATA,sha256=B7yPgikOYilTBVwdJs9VXdrdzONDc5JFXPX7cRoyYZI,3510
32
+ ecodev_core-0.0.54.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
33
+ ecodev_core-0.0.54.dist-info/RECORD,,