ecodev-core 0.0.1__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 +79 -0
- ecodev_core/app_activity.py +108 -0
- ecodev_core/app_rights.py +24 -0
- ecodev_core/app_user.py +92 -0
- ecodev_core/auth_configuration.py +22 -0
- ecodev_core/authentication.py +226 -0
- ecodev_core/check_dependencies.py +179 -0
- ecodev_core/custom_equal.py +27 -0
- ecodev_core/db_connection.py +68 -0
- ecodev_core/db_filters.py +142 -0
- ecodev_core/db_insertion.py +108 -0
- ecodev_core/db_retrieval.py +194 -0
- ecodev_core/enum_utils.py +21 -0
- ecodev_core/list_utils.py +65 -0
- ecodev_core/logger.py +106 -0
- ecodev_core/pandas_utils.py +30 -0
- ecodev_core/permissions.py +15 -0
- ecodev_core/pydantic_utils.py +52 -0
- ecodev_core/read_write.py +40 -0
- ecodev_core/safe_utils.py +197 -0
- ecodev_core-0.0.1.dist-info/LICENSE.md +11 -0
- ecodev_core-0.0.1.dist-info/METADATA +72 -0
- ecodev_core-0.0.1.dist-info/RECORD +24 -0
- ecodev_core-0.0.1.dist-info/WHEEL +4 -0
ecodev_core/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module listing all public method from the ecodev_core modules
|
|
3
|
+
"""
|
|
4
|
+
from ecodev_core.app_activity import AppActivity
|
|
5
|
+
from ecodev_core.app_activity import dash_monitor
|
|
6
|
+
from ecodev_core.app_activity import fastapi_monitor
|
|
7
|
+
from ecodev_core.app_activity import get_method
|
|
8
|
+
from ecodev_core.app_activity import get_recent_activities
|
|
9
|
+
from ecodev_core.app_rights import AppRight
|
|
10
|
+
from ecodev_core.app_user import AppUser
|
|
11
|
+
from ecodev_core.app_user import select_user
|
|
12
|
+
from ecodev_core.app_user import upsert_app_users
|
|
13
|
+
from ecodev_core.auth_configuration import AUTH
|
|
14
|
+
from ecodev_core.authentication import attempt_to_log
|
|
15
|
+
from ecodev_core.authentication import get_access_token
|
|
16
|
+
from ecodev_core.authentication import get_app_services
|
|
17
|
+
from ecodev_core.authentication import get_current_user
|
|
18
|
+
from ecodev_core.authentication import get_user
|
|
19
|
+
from ecodev_core.authentication import is_admin_user
|
|
20
|
+
from ecodev_core.authentication import is_authorized_user
|
|
21
|
+
from ecodev_core.authentication import is_monitoring_user
|
|
22
|
+
from ecodev_core.authentication import JwtAuth
|
|
23
|
+
from ecodev_core.authentication import SCHEME
|
|
24
|
+
from ecodev_core.authentication import Token
|
|
25
|
+
from ecodev_core.check_dependencies import check_dependencies
|
|
26
|
+
from ecodev_core.check_dependencies import compute_dependencies
|
|
27
|
+
from ecodev_core.custom_equal import custom_equal
|
|
28
|
+
from ecodev_core.db_connection import create_db_and_tables
|
|
29
|
+
from ecodev_core.db_connection import DB_URL
|
|
30
|
+
from ecodev_core.db_connection import delete_table
|
|
31
|
+
from ecodev_core.db_connection import engine
|
|
32
|
+
from ecodev_core.db_connection import get_session
|
|
33
|
+
from ecodev_core.db_connection import info_message
|
|
34
|
+
from ecodev_core.db_filters import ServerSideFilter
|
|
35
|
+
from ecodev_core.db_insertion import generic_insertion
|
|
36
|
+
from ecodev_core.db_insertion import get_raw_df
|
|
37
|
+
from ecodev_core.db_retrieval import count_rows
|
|
38
|
+
from ecodev_core.db_retrieval import get_rows
|
|
39
|
+
from ecodev_core.db_retrieval import ServerSideField
|
|
40
|
+
from ecodev_core.enum_utils import enum_converter
|
|
41
|
+
from ecodev_core.list_utils import first_or_default
|
|
42
|
+
from ecodev_core.list_utils import first_transformed_or_default
|
|
43
|
+
from ecodev_core.list_utils import group_by_value
|
|
44
|
+
from ecodev_core.list_utils import lselect
|
|
45
|
+
from ecodev_core.list_utils import lselectfirst
|
|
46
|
+
from ecodev_core.logger import log_critical
|
|
47
|
+
from ecodev_core.logger import logger_get
|
|
48
|
+
from ecodev_core.pandas_utils import jsonify_series
|
|
49
|
+
from ecodev_core.pandas_utils import pd_equals
|
|
50
|
+
from ecodev_core.permissions import Permission
|
|
51
|
+
from ecodev_core.pydantic_utils import Basic
|
|
52
|
+
from ecodev_core.pydantic_utils import CustomFrozen
|
|
53
|
+
from ecodev_core.pydantic_utils import Frozen
|
|
54
|
+
from ecodev_core.pydantic_utils import OrmFrozen
|
|
55
|
+
from ecodev_core.read_write import load_json_file
|
|
56
|
+
from ecodev_core.read_write import make_dir
|
|
57
|
+
from ecodev_core.read_write import write_json_file
|
|
58
|
+
from ecodev_core.safe_utils import boolify
|
|
59
|
+
from ecodev_core.safe_utils import floatify
|
|
60
|
+
from ecodev_core.safe_utils import intify
|
|
61
|
+
from ecodev_core.safe_utils import safe_clt
|
|
62
|
+
from ecodev_core.safe_utils import SafeTestCase
|
|
63
|
+
from ecodev_core.safe_utils import SimpleReturn
|
|
64
|
+
from ecodev_core.safe_utils import stringify
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
'AUTH', 'Token', 'get_app_services', 'attempt_to_log', 'get_current_user', 'is_admin_user',
|
|
69
|
+
'write_json_file', 'load_json_file', 'make_dir', 'check_dependencies', 'compute_dependencies',
|
|
70
|
+
'engine', 'create_db_and_tables', 'get_session', 'info_message', 'group_by_value', 'OrmFrozen',
|
|
71
|
+
'first_or_default', 'lselect', 'lselectfirst', 'first_transformed_or_default', 'log_critical',
|
|
72
|
+
'logger_get', 'Permission', 'AppUser', 'AppRight', 'Basic', 'Frozen', 'CustomFrozen', 'JwtAuth',
|
|
73
|
+
'SafeTestCase', 'SimpleReturn', 'safe_clt', 'stringify', 'boolify', 'get_user', 'floatify',
|
|
74
|
+
'delete_table', 'SCHEME', 'DB_URL', 'pd_equals', 'jsonify_series', 'upsert_app_users', 'intify',
|
|
75
|
+
'enum_converter', 'ServerSideFilter', 'get_rows', 'count_rows', 'ServerSideField', 'get_raw_df',
|
|
76
|
+
'generic_insertion', 'custom_equal', 'is_authorized_user', 'get_method', 'AppActivity',
|
|
77
|
+
'fastapi_monitor', 'dash_monitor', 'is_monitoring_user', 'get_recent_activities', 'select_user',
|
|
78
|
+
'get_access_token'
|
|
79
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing a simple monitoring table
|
|
3
|
+
"""
|
|
4
|
+
import inspect
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from sqlmodel import col
|
|
10
|
+
from sqlmodel import Field
|
|
11
|
+
from sqlmodel import select
|
|
12
|
+
from sqlmodel import Session
|
|
13
|
+
from sqlmodel import SQLModel
|
|
14
|
+
|
|
15
|
+
from ecodev_core.app_user import AppUser
|
|
16
|
+
from ecodev_core.authentication import get_user
|
|
17
|
+
from ecodev_core.db_connection import engine
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
Simple helper to retrieve the method name in which this helper is called
|
|
22
|
+
|
|
23
|
+
NB: this is meant to stay a lambda, otherwise the name retrieved is get_method, not the caller
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_method(): return inspect.stack()[1][3]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AppActivityBase(SQLModel):
|
|
31
|
+
"""
|
|
32
|
+
Simple monitoring class
|
|
33
|
+
|
|
34
|
+
Attributes are:
|
|
35
|
+
- user: the name of the user that triggered the monitoring log
|
|
36
|
+
- application: the application in which the user triggered the monitoring log
|
|
37
|
+
- method: the method called by the user that triggered the monitoring log
|
|
38
|
+
- relevant_option: if filled, complementary information on method (num of treated lines...)
|
|
39
|
+
"""
|
|
40
|
+
user: str = Field(index=True)
|
|
41
|
+
application: str = Field(index=True)
|
|
42
|
+
method: str = Field(index=True)
|
|
43
|
+
relevant_option: Optional[str] = Field(index=True, default=None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AppActivity(AppActivityBase, table=True): # type: ignore
|
|
47
|
+
"""
|
|
48
|
+
The table version of the AppActivityBase monitoring class
|
|
49
|
+
"""
|
|
50
|
+
__tablename__ = 'app_activity'
|
|
51
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
52
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def dash_monitor(method: str,
|
|
56
|
+
token: Dict,
|
|
57
|
+
application: str,
|
|
58
|
+
relevant_option: Optional[str] = None):
|
|
59
|
+
"""
|
|
60
|
+
Generic dash monitor.
|
|
61
|
+
|
|
62
|
+
Attributes are:
|
|
63
|
+
- method: the method called by the user that triggered the monitoring log
|
|
64
|
+
- token: contains the information on the user that triggered the monitoring log
|
|
65
|
+
- application: the application in which the user triggered the monitoring log
|
|
66
|
+
- relevant_option: if filled, complementary information on method (num of treated lines...)
|
|
67
|
+
"""
|
|
68
|
+
with Session(engine) as session:
|
|
69
|
+
user = get_user(token.get('token', {}).get('access_token'))
|
|
70
|
+
add_activity_to_db(method, user, application, session, relevant_option)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fastapi_monitor(method: str,
|
|
74
|
+
user: AppUser,
|
|
75
|
+
application: str,
|
|
76
|
+
session: Session,
|
|
77
|
+
relevant_option: Optional[str] = None):
|
|
78
|
+
"""
|
|
79
|
+
Generic fastapi monitor.
|
|
80
|
+
|
|
81
|
+
Attributes are:
|
|
82
|
+
- method: the method called by the user that triggered the monitoring log
|
|
83
|
+
- user: the name of the user that triggered the monitoring log
|
|
84
|
+
- application: the application in which the user triggered the monitoring log
|
|
85
|
+
- session: db connection
|
|
86
|
+
- relevant_option: if filled, complementary information on method (num of treated lines...)
|
|
87
|
+
"""
|
|
88
|
+
add_activity_to_db(method, user, application, session, relevant_option)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def add_activity_to_db(method: str,
|
|
92
|
+
user: AppUser,
|
|
93
|
+
application: str,
|
|
94
|
+
session: Session,
|
|
95
|
+
relevant_option: Optional[str] = None):
|
|
96
|
+
"""
|
|
97
|
+
Add a new entry in AppActivity given the passed arguments
|
|
98
|
+
"""
|
|
99
|
+
session.add(AppActivity(user=user.user, application=application, method=method,
|
|
100
|
+
relevant_option=relevant_option))
|
|
101
|
+
session.commit()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_recent_activities(last_date: str, session: Session):
|
|
105
|
+
"""
|
|
106
|
+
Returns all activities that happened after last_date
|
|
107
|
+
"""
|
|
108
|
+
return session.exec(select(AppActivity).where(col(AppActivity.created_at) > last_date)).all()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing the sqlmodel orm part of the right table
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Field
|
|
8
|
+
from sqlmodel import Relationship
|
|
9
|
+
from sqlmodel import SQLModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
from ecodev_core.app_user import AppUser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AppRight(SQLModel, table=True): # type: ignore
|
|
17
|
+
"""
|
|
18
|
+
Simple right class: listing all app_services that a particular user can access to
|
|
19
|
+
"""
|
|
20
|
+
__tablename__ = 'app_right'
|
|
21
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
22
|
+
app_service: str
|
|
23
|
+
user_id: Optional[int] = Field(default=None, foreign_key='app_user.id')
|
|
24
|
+
user: Optional['AppUser'] = Relationship(back_populates='rights')
|
ecodev_core/app_user.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing the sqlmodel orm part of the user table
|
|
3
|
+
"""
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from typing import List
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from sqlmodel import col
|
|
12
|
+
from sqlmodel import Field
|
|
13
|
+
from sqlmodel import Relationship
|
|
14
|
+
from sqlmodel import select
|
|
15
|
+
from sqlmodel import Session
|
|
16
|
+
from sqlmodel import SQLModel
|
|
17
|
+
from sqlmodel.sql.expression import SelectOfScalar
|
|
18
|
+
|
|
19
|
+
from ecodev_core.db_insertion import create_or_update
|
|
20
|
+
from ecodev_core.db_insertion import Insertor
|
|
21
|
+
from ecodev_core.permissions import Permission
|
|
22
|
+
from ecodev_core.read_write import load_json_file
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
26
|
+
from ecodev_core.app_rights import AppRight
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AppUser(SQLModel, table=True): # type: ignore
|
|
30
|
+
"""
|
|
31
|
+
Simple user class: an id associate to a user with a password
|
|
32
|
+
"""
|
|
33
|
+
__tablename__ = 'app_user'
|
|
34
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
35
|
+
user: str = Field(index=True)
|
|
36
|
+
password: str
|
|
37
|
+
permission: Permission = Field(default=Permission.ADMIN)
|
|
38
|
+
client: Optional[str] = Field(default=None)
|
|
39
|
+
rights: List['AppRight'] = Relationship(back_populates='user')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def user_convertor(df: pd.DataFrame) -> List[Dict]:
|
|
43
|
+
"""
|
|
44
|
+
Dummy user convertor
|
|
45
|
+
"""
|
|
46
|
+
return [x for _, x in df.iterrows()]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def user_reductor(in_db_row: AppUser, db_row: AppUser) -> AppUser:
|
|
50
|
+
"""
|
|
51
|
+
Update an existing in_db_row with new information coming from db_row
|
|
52
|
+
|
|
53
|
+
NB: in the future this will maybe handle the client as well
|
|
54
|
+
"""
|
|
55
|
+
in_db_row.permission = db_row.permission
|
|
56
|
+
in_db_row.password = db_row.password
|
|
57
|
+
return in_db_row
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def user_selector(db_row: AppUser) -> SelectOfScalar:
|
|
61
|
+
"""
|
|
62
|
+
Criteria on which to decide whether creating a new row or updating an existing one in db
|
|
63
|
+
"""
|
|
64
|
+
return select(AppUser).where(col(AppUser.user) == db_row.user)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
USER_INSERTOR = Insertor(convertor=user_convertor, selector=user_selector,
|
|
68
|
+
reductor=user_reductor, db_schema=AppUser, read_excel_file=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def upsert_app_users(file_path: Path, session: Session) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Upsert db users with a list of users provided in the file_path (json format)
|
|
74
|
+
"""
|
|
75
|
+
for user in load_json_file(file_path):
|
|
76
|
+
session.add(create_or_update(session, user, USER_INSERTOR))
|
|
77
|
+
session.commit()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def select_user(username: str, session: Session) -> AppUser:
|
|
81
|
+
"""
|
|
82
|
+
Helper function to (attempt to) retrieve AppUser from username.
|
|
83
|
+
|
|
84
|
+
NB: Used to check whether user exists, before resetting its password.
|
|
85
|
+
I.e. User does not yet have a token - we are simply checking if
|
|
86
|
+
an active account exists under that username.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
sqlalchemy.exc.NoResultFound: Typical error is no users are found.
|
|
90
|
+
sqlalchemy.exc.MultipleResultsFound: Should normally never be an issue.
|
|
91
|
+
"""
|
|
92
|
+
return session.exec(select(AppUser).where(col(AppUser.user) == username)).one()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing authentication configuration.
|
|
3
|
+
"""
|
|
4
|
+
from pydantic import BaseSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthenticationConfiguration(BaseSettings):
|
|
8
|
+
"""
|
|
9
|
+
Simple authentication configuration class
|
|
10
|
+
"""
|
|
11
|
+
secret_key: str
|
|
12
|
+
algorithm: str
|
|
13
|
+
access_token_expire_minutes: int
|
|
14
|
+
|
|
15
|
+
class Config:
|
|
16
|
+
"""
|
|
17
|
+
Config class specifying the name of the environment file to read
|
|
18
|
+
"""
|
|
19
|
+
env_file = '.env'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
AUTH = AuthenticationConfiguration()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing all jwt security logic
|
|
3
|
+
"""
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from datetime import timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import Dict
|
|
9
|
+
from typing import List
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
from fastapi import APIRouter
|
|
14
|
+
from fastapi import Depends
|
|
15
|
+
from fastapi import HTTPException
|
|
16
|
+
from fastapi import status
|
|
17
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
18
|
+
from jose import jwt
|
|
19
|
+
from jose import JWTError
|
|
20
|
+
from passlib.context import CryptContext
|
|
21
|
+
from sqladmin.authentication import AuthenticationBackend
|
|
22
|
+
from sqlmodel import col
|
|
23
|
+
from sqlmodel import select
|
|
24
|
+
from sqlmodel import Session
|
|
25
|
+
from starlette.requests import Request
|
|
26
|
+
from starlette.responses import RedirectResponse
|
|
27
|
+
|
|
28
|
+
from ecodev_core.app_user import AppUser
|
|
29
|
+
from ecodev_core.auth_configuration import AUTH
|
|
30
|
+
from ecodev_core.db_connection import engine
|
|
31
|
+
from ecodev_core.logger import logger_get
|
|
32
|
+
from ecodev_core.permissions import Permission
|
|
33
|
+
from ecodev_core.pydantic_utils import Frozen
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
SCHEME = OAuth2PasswordBearer(tokenUrl='login')
|
|
37
|
+
auth_router = APIRouter(tags=['authentication'])
|
|
38
|
+
CONTEXT = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
|
39
|
+
MONITORING = 'monitoring'
|
|
40
|
+
MONITORING_ERROR = 'Could not validate credentials. You need to be the monitoring user to call this'
|
|
41
|
+
INVALID_USER = 'Invalid User'
|
|
42
|
+
ADMIN_ERROR = 'Could not validate credentials. You need admin rights to call this'
|
|
43
|
+
INVALID_CREDENTIALS = 'Invalid Credentials'
|
|
44
|
+
log = logger_get(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Token(Frozen):
|
|
48
|
+
"""
|
|
49
|
+
Simple class for storing token value and type
|
|
50
|
+
"""
|
|
51
|
+
access_token: str
|
|
52
|
+
token_type: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TokenData(Frozen):
|
|
56
|
+
"""
|
|
57
|
+
Simple class storing token id information
|
|
58
|
+
"""
|
|
59
|
+
id: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_access_token(token: Dict[str, Any]):
|
|
63
|
+
"""
|
|
64
|
+
Robust method to return access token or None
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
return token.get('token', {}).get('access_token')
|
|
68
|
+
except AttributeError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_app_services(user: AppUser, session: Session) -> List[str]:
|
|
73
|
+
"""
|
|
74
|
+
Retrieve all app services the passed user has access to
|
|
75
|
+
"""
|
|
76
|
+
if db_user := session.exec(select(AppUser).where(col(AppUser.id) == user.id)).first():
|
|
77
|
+
return [right.app_service for right in db_user.rights]
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class JwtAuth(AuthenticationBackend):
|
|
82
|
+
"""
|
|
83
|
+
Sqladmin security class. Implement login/logout procedure as well as the authentication check.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
async def login(self, request: Request) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Login procedure: factorized with the fastapi jwt logic
|
|
89
|
+
"""
|
|
90
|
+
form = await request.form()
|
|
91
|
+
if token := self.authorized(form):
|
|
92
|
+
request.session.update(token)
|
|
93
|
+
return True if token else False
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def authorized(form: Dict):
|
|
97
|
+
"""
|
|
98
|
+
Check that the user information contained in the form corresponds to an admin user
|
|
99
|
+
"""
|
|
100
|
+
with Session(engine) as session:
|
|
101
|
+
try:
|
|
102
|
+
token = attempt_to_log(form.get('username', ''), form.get('password', ''), session)
|
|
103
|
+
if is_admin_user(token['access_token']):
|
|
104
|
+
return token
|
|
105
|
+
return None
|
|
106
|
+
except HTTPException:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
async def logout(self, request: Request) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Logout procedure: clears the cache
|
|
112
|
+
"""
|
|
113
|
+
request.session.clear()
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
async def authenticate(self, request: Request) -> Optional[RedirectResponse]:
|
|
117
|
+
"""
|
|
118
|
+
Authentication procedure
|
|
119
|
+
"""
|
|
120
|
+
return (token := request.session.get('access_token')) and is_admin_user(token)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def attempt_to_log(user: str,
|
|
124
|
+
password: str,
|
|
125
|
+
session: Session) -> Union[Dict, HTTPException]:
|
|
126
|
+
"""
|
|
127
|
+
Factorized security logic. Ensure that the user is a legit one with a valid password
|
|
128
|
+
"""
|
|
129
|
+
selector = select(AppUser).where(col(AppUser.user) == user)
|
|
130
|
+
if not (db_user := session.exec(selector).first()):
|
|
131
|
+
log.warning('unauthorized user')
|
|
132
|
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=INVALID_USER)
|
|
133
|
+
if not _check_password(password, db_user.password):
|
|
134
|
+
log.warning('invalid user')
|
|
135
|
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=INVALID_CREDENTIALS)
|
|
136
|
+
|
|
137
|
+
return {'access_token': _create_access_token(data={'user_id': db_user.id}),
|
|
138
|
+
'token_type': 'bearer'}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_authorized_user(token: str = Depends(SCHEME)) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Check if the passed token corresponds to an authorized user
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
return get_current_user(token) is not None
|
|
147
|
+
except Exception:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_user(token: str = Depends(SCHEME)) -> AppUser:
|
|
152
|
+
"""
|
|
153
|
+
Retrieves (if it exists) the db user corresponding to the passed token
|
|
154
|
+
"""
|
|
155
|
+
if user := get_current_user(token):
|
|
156
|
+
return user
|
|
157
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_CREDENTIALS,
|
|
158
|
+
headers={'WWW-Authenticate': 'Bearer'})
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_current_user(token: str) -> Union[AppUser, None]:
|
|
162
|
+
"""
|
|
163
|
+
Retrieves (if it exists) a valid (meaning who has valid credentials) user from the db
|
|
164
|
+
"""
|
|
165
|
+
token = _verify_access_token(token)
|
|
166
|
+
with Session(engine) as session:
|
|
167
|
+
return session.exec(select(AppUser).where(col(AppUser.id) == token.id)).first()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def is_admin_user(token: str = Depends(SCHEME)) -> AppUser:
|
|
171
|
+
"""
|
|
172
|
+
Retrieves (if it exists) the admin (meaning who has valid credentials) user from the db
|
|
173
|
+
"""
|
|
174
|
+
if (user := get_current_user(token)) and user.permission == Permission.ADMIN:
|
|
175
|
+
return user
|
|
176
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ADMIN_ERROR,
|
|
177
|
+
headers={'WWW-Authenticate': 'Bearer'})
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def is_monitoring_user(token: str = Depends(SCHEME)) -> AppUser:
|
|
181
|
+
"""
|
|
182
|
+
Retrieves (if it exists) the monitoring user from the db
|
|
183
|
+
"""
|
|
184
|
+
if (user := get_current_user(token)) and user.user == MONITORING:
|
|
185
|
+
return user
|
|
186
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
|
187
|
+
detail=MONITORING_ERROR, headers={'WWW-Authenticate': 'Bearer'})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _create_access_token(data: Dict) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Create an access token out of the passed data. Only called if credentials are valid
|
|
193
|
+
"""
|
|
194
|
+
to_encode = data.copy()
|
|
195
|
+
expire = datetime.now(timezone.utc) + timedelta(minutes=AUTH.access_token_expire_minutes)
|
|
196
|
+
to_encode.update({'exp': expire})
|
|
197
|
+
return jwt.encode(to_encode, AUTH.secret_key, algorithm=AUTH.algorithm)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _verify_access_token(token: str) -> TokenData:
|
|
201
|
+
"""
|
|
202
|
+
Retrieves the token data associated to the passed token if it contains valid credential info.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
payload = jwt.decode(token, AUTH.secret_key, algorithms=[AUTH.algorithm])
|
|
206
|
+
if (user_id := payload.get('user_id')) is None:
|
|
207
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_USER,
|
|
208
|
+
headers={'WWW-Authenticate': 'Bearer'})
|
|
209
|
+
return TokenData(id=user_id)
|
|
210
|
+
except JWTError as e:
|
|
211
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_CREDENTIALS,
|
|
212
|
+
headers={'WWW-Authenticate': 'Bearer'}) from e
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _hash_password(password: str) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Hashes the passed password (encoding).
|
|
218
|
+
"""
|
|
219
|
+
return CONTEXT.hash(password)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _check_password(plain_password: str, hashed_password: str) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
Check the passed password (compare it to the passed encoded one).
|
|
225
|
+
"""
|
|
226
|
+
return CONTEXT.verify(plain_password, hashed_password)
|