From 80239a08df9ebd806cb3b402a2a4cf82e1173045 Mon Sep 17 00:00:00 2001 From: John Grubba Date: Fri, 26 Jul 2024 08:36:21 +0200 Subject: [PATCH 1/6] Restructure Config Loader --- src/api/oauth_providers/__init__.py | 2 +- src/api/oauth_providers/github.py | 2 +- src/api/profile.py | 2 +- src/crud/sessions.py | 2 +- src/tools/__init__.py | 11 +-- src/tools/conf.py | 86 ------------------------ src/tools/conf/AccountFeaturesConfig.py | 20 ++++++ src/tools/conf/EmailConfig.py | 9 +++ src/tools/conf/InternalConfig.py | 16 +++++ src/tools/conf/SecurityConfig.py | 9 +++ src/tools/conf/SessionConfig.py | 10 +++ src/tools/conf/SignupConfig.py | 34 ++++++++++ src/tools/conf/__init__.py | 18 +++++ src/tools/conf/conf.py | 18 +++++ src/tools/{ => conf}/testing_config.json | 0 src/tools/db.py | 2 +- src/tools/mail.py | 2 +- test.py | 10 +++ 18 files changed, 151 insertions(+), 102 deletions(-) delete mode 100644 src/tools/conf.py create mode 100644 src/tools/conf/AccountFeaturesConfig.py create mode 100644 src/tools/conf/EmailConfig.py create mode 100644 src/tools/conf/InternalConfig.py create mode 100644 src/tools/conf/SecurityConfig.py create mode 100644 src/tools/conf/SessionConfig.py create mode 100644 src/tools/conf/SignupConfig.py create mode 100644 src/tools/conf/__init__.py create mode 100644 src/tools/conf/conf.py rename src/tools/{ => conf}/testing_config.json (100%) create mode 100644 test.py diff --git a/src/api/oauth_providers/__init__.py b/src/api/oauth_providers/__init__.py index 380cc14..ee8c11c 100644 --- a/src/api/oauth_providers/__init__.py +++ b/src/api/oauth_providers/__init__.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from tools.conf import SignupConfig +from tools import SignupConfig router = APIRouter( prefix="/oauth", diff --git a/src/api/oauth_providers/github.py b/src/api/oauth_providers/github.py index 6e83b39..6312c9c 100644 --- a/src/api/oauth_providers/github.py +++ b/src/api/oauth_providers/github.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Request, BackgroundTasks, Response, HTTPException from fastapi.responses import RedirectResponse -from tools.conf import SignupConfig, SessionConfig +from tools import SignupConfig, SessionConfig import json import requests import re diff --git a/src/api/profile.py b/src/api/profile.py index dbcfdab..5cba226 100644 --- a/src/api/profile.py +++ b/src/api/profile.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Response -from tools.conf import AccountFeaturesConfig, SignupConfig +from tools import AccountFeaturesConfig, SignupConfig from api.model import ResetPasswordRequest, ConfirmEmailRequest, DeleteAccountRequest import json import bcrypt diff --git a/src/crud/sessions.py b/src/crud/sessions.py index 14bcb12..0c986ee 100644 --- a/src/crud/sessions.py +++ b/src/crud/sessions.py @@ -1,7 +1,7 @@ import uuid from tools import sessions_collection, users_collection import datetime -from tools.conf import SessionConfig +from tools import SessionConfig from fastapi import Request from user_agents import parse from bson import ObjectId, errors diff --git a/src/tools/__init__.py b/src/tools/__init__.py index f815a2c..e27e61c 100644 --- a/src/tools/__init__.py +++ b/src/tools/__init__.py @@ -1,14 +1,5 @@ from .db import users_collection, sessions_collection, bson_to_json, r -from .conf import ( - SignupConfig, - EmailConfig, - SessionConfig, - InternalConfig, - AccountFeaturesConfig, - insecure_cols, - SecurityConfig, - default_signup_fields, -) +from .conf import * from .mail import send_email, broadcast_emails from .confirmation_codes import all_ids, regenerate_ids diff --git a/src/tools/conf.py b/src/tools/conf.py deleted file mode 100644 index 35db699..0000000 --- a/src/tools/conf.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -from collections import ChainMap -import sys -import os - -__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) - -if "pytest" in sys.modules: - # Running Tests (Load Testing Config) - config = json.load(open(os.path.join(__location__, "testing_config.json"), "rb")) -else: - # Normal Startup - config = json.load(open("/src/app/config/config.json", "rb")) - -# Columns that should never leave EZAuth (maybe get more in the future) -default_signup_fields = {"username", "email", "password"} -insecure_cols = {"password": 0, "2fa_secret": 0, "google_uid": 0, "github_uid": 0} -not_updateable_cols_internal = ["email", "createdAt", "expireAt"] -# Columns that can leave EZAuth but should only be used internally can be defined in config - - -class SignupConfig: - enable_conf_email: bool = config["signup"]["enable_conf_email"] - conf_code_expiry: int = config["signup"]["conf_code_expiry"] - conf_code_complexity: int = config["signup"]["conf_code_complexity"] - enable_welcome_email: bool = config["signup"]["enable_welcome_email"] - oauth_providers: list[str] = config["signup"]["oauth"]["providers_enabled"] - oauth_base_url: str = str(config["signup"]["oauth"]["base_url"]).removesuffix("/") - - -class EmailConfig: - login_usr: str = config["email"]["login_usr"] - login_pwd: str = config["email"]["login_pwd"] - sender_email: str = config["email"]["sender_email"] - smtp_host: str = config["email"]["smtp_host"] - smtp_port: int = config["email"]["smtp_port"] - - -class SessionConfig: - session_expiry_seconds: int = config["session"]["session_expiry_seconds"] - max_session_count: int = config["session"]["max_session_count"] - auto_cookie: bool = config["session"]["auto_cookie"] - auto_cookie_name: str = config["session"]["auto_cookie_name"] - cookie_samesite: str = config["session"]["cookie_samesite"] - cookie_secure: bool = config["session"]["cookie_secure"] - - -class InternalConfig: - internal_api_key: str = config["internal"]["internal_api_key"] - internal_columns: dict = dict( - ChainMap(*[{col: 0} for col in config["internal"]["internal_columns"]]) - ) - internal_columns.update(insecure_cols) - # Insecure Cols + Internal Cols can't be updated by the user - not_updateable_columns: list = ( - config["internal"]["not_updateable_columns"] - + list(internal_columns.keys()) - + not_updateable_cols_internal - ) - - -class AccountFeaturesConfig: - enable_reset_pswd: bool = config["account_features"]["enable_reset_pswd"] - reset_pswd_conf_mail: bool = config["account_features"]["reset_pswd_conf_mail"] - enable_2fa: bool = config["account_features"]["2fa"]["enable"] - issuer_name_2fa: str = config["account_features"]["2fa"]["issuer_name"] - issuer_image_url_2fa: str = config["account_features"]["2fa"]["issuer_image_url"] - qr_code_endpoint_2fa: bool = config["account_features"]["2fa"]["qr_endpoint"] - allow_add_fields_on_signup: set[str] = set( - config["account_features"]["allow_add_fields_on_signup"] - ) - set(not_updateable_cols_internal) - allow_add_fields_patch_user: set[str] = set( - config["account_features"]["allow_add_fields_patch_user"] - ) - set(not_updateable_cols_internal) - allow_deletion: bool = config["account_features"]["allow_deletion"] - deletion_pending_minutes: int = config["account_features"][ - "deletion_pending_minutes" - ] - - -class SecurityConfig: - access_control_origins: set[str] = set(config["security"]["allow_origins"]) - allow_headers: set[str] = set(config["security"]["allow_headers"]) - max_login_attempts: int = config["security"]["max_login_attempts"] - login_timeout: int = config["security"]["login_timeout"] - expire_unfinished_timeout: int = config["security"]["expire_unfinished_timeout"] diff --git a/src/tools/conf/AccountFeaturesConfig.py b/src/tools/conf/AccountFeaturesConfig.py new file mode 100644 index 0000000..79033ed --- /dev/null +++ b/src/tools/conf/AccountFeaturesConfig.py @@ -0,0 +1,20 @@ +from .conf import config, not_updateable_cols_internal + + +class AccountFeaturesConfig: + enable_reset_pswd: bool = config["account_features"]["enable_reset_pswd"] + reset_pswd_conf_mail: bool = config["account_features"]["reset_pswd_conf_mail"] + enable_2fa: bool = config["account_features"]["2fa"]["enable"] + issuer_name_2fa: str = config["account_features"]["2fa"]["issuer_name"] + issuer_image_url_2fa: str = config["account_features"]["2fa"]["issuer_image_url"] + qr_code_endpoint_2fa: bool = config["account_features"]["2fa"]["qr_endpoint"] + allow_add_fields_on_signup: set[str] = set( + config["account_features"]["allow_add_fields_on_signup"] + ) - set(not_updateable_cols_internal) + allow_add_fields_patch_user: set[str] = set( + config["account_features"]["allow_add_fields_patch_user"] + ) - set(not_updateable_cols_internal) + allow_deletion: bool = config["account_features"]["allow_deletion"] + deletion_pending_minutes: int = config["account_features"][ + "deletion_pending_minutes" + ] diff --git a/src/tools/conf/EmailConfig.py b/src/tools/conf/EmailConfig.py new file mode 100644 index 0000000..27fb70f --- /dev/null +++ b/src/tools/conf/EmailConfig.py @@ -0,0 +1,9 @@ +from .conf import config + + +class EmailConfig: + login_usr: str = config["email"]["login_usr"] + login_pwd: str = config["email"]["login_pwd"] + sender_email: str = config["email"]["sender_email"] + smtp_host: str = config["email"]["smtp_host"] + smtp_port: int = config["email"]["smtp_port"] diff --git a/src/tools/conf/InternalConfig.py b/src/tools/conf/InternalConfig.py new file mode 100644 index 0000000..9f496b5 --- /dev/null +++ b/src/tools/conf/InternalConfig.py @@ -0,0 +1,16 @@ +from .conf import config, insecure_cols, not_updateable_cols_internal +from collections import ChainMap + + +class InternalConfig: + internal_api_key: str = config["internal"]["internal_api_key"] + internal_columns: dict = dict( + ChainMap(*[{col: 0} for col in config["internal"]["internal_columns"]]) + ) + internal_columns.update(insecure_cols) + # Insecure Cols + Internal Cols can't be updated by the user + not_updateable_columns: list = ( + config["internal"]["not_updateable_columns"] + + list(internal_columns.keys()) + + not_updateable_cols_internal + ) diff --git a/src/tools/conf/SecurityConfig.py b/src/tools/conf/SecurityConfig.py new file mode 100644 index 0000000..6aa61c7 --- /dev/null +++ b/src/tools/conf/SecurityConfig.py @@ -0,0 +1,9 @@ +from .conf import config + + +class SecurityConfig: + access_control_origins: set[str] = set(config["security"]["allow_origins"]) + allow_headers: set[str] = set(config["security"]["allow_headers"]) + max_login_attempts: int = config["security"]["max_login_attempts"] + login_timeout: int = config["security"]["login_timeout"] + expire_unfinished_timeout: int = config["security"]["expire_unfinished_timeout"] diff --git a/src/tools/conf/SessionConfig.py b/src/tools/conf/SessionConfig.py new file mode 100644 index 0000000..85624da --- /dev/null +++ b/src/tools/conf/SessionConfig.py @@ -0,0 +1,10 @@ +from .conf import config + + +class SessionConfig: + session_expiry_seconds: int = config["session"]["session_expiry_seconds"] + max_session_count: int = config["session"]["max_session_count"] + auto_cookie: bool = config["session"]["auto_cookie"] + auto_cookie_name: str = config["session"]["auto_cookie_name"] + cookie_samesite: str = config["session"]["cookie_samesite"] + cookie_secure: bool = config["session"]["cookie_secure"] diff --git a/src/tools/conf/SignupConfig.py b/src/tools/conf/SignupConfig.py new file mode 100644 index 0000000..95f25ee --- /dev/null +++ b/src/tools/conf/SignupConfig.py @@ -0,0 +1,34 @@ +from .conf import config + + +class SignupConfig: + enable_conf_email: bool = config["signup"]["enable_conf_email"] + conf_code_expiry: int = config["signup"]["conf_code_expiry"] + conf_code_complexity: int = config["signup"]["conf_code_complexity"] + enable_welcome_email: bool = config["signup"]["enable_welcome_email"] + oauth_providers: list[str] = config["signup"]["oauth"]["providers_enabled"] + oauth_base_url: str = str(config["signup"]["oauth"]["base_url"]).removesuffix("/") + + def validate(self) -> bool: + """This is to Type Check the Configuration + + Raises: + ValueError: _description_ + ValueError: _description_ + + Returns: + bool: _description_ + """ + if type(self.enable_conf_email) != bool: + raise ValueError( + "signup.enable_conf_email must be a boolean (got type {})".format( + type(self.enable_conf_email) + ) + ) + if type(self.conf_code_expiry) != int: + raise ValueError( + "signup.conf_code_expiry must be an integer (got type {})".format( + type(self.conf_code_expiry) + ) + ) + pass diff --git a/src/tools/conf/__init__.py b/src/tools/conf/__init__.py new file mode 100644 index 0000000..ba7a6a7 --- /dev/null +++ b/src/tools/conf/__init__.py @@ -0,0 +1,18 @@ +from .SignupConfig import SignupConfig +from .EmailConfig import EmailConfig +from .AccountFeaturesConfig import AccountFeaturesConfig +from .InternalConfig import InternalConfig +from .SecurityConfig import SecurityConfig +from .SessionConfig import SessionConfig +from .conf import insecure_cols, default_signup_fields + +__all__ = [ + "SignupConfig", + "EmailConfig", + "AccountFeaturesConfig", + "InternalConfig", + "SecurityConfig", + "SessionConfig", + "insecure_cols", + "default_signup_fields", +] diff --git a/src/tools/conf/conf.py b/src/tools/conf/conf.py new file mode 100644 index 0000000..585d91a --- /dev/null +++ b/src/tools/conf/conf.py @@ -0,0 +1,18 @@ +import json +import sys +import os + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + +if "pytest" in sys.modules: + # Running Tests (Load Testing Config) + config = json.load(open(os.path.join(__location__, "testing_config.json"), "rb")) +else: + # Normal Startup + config = json.load(open("/src/app/config/config.json", "rb")) + +# Columns that should never leave EZAuth (maybe get more in the future) +default_signup_fields = {"username", "email", "password"} +insecure_cols = {"password": 0, "2fa_secret": 0, "google_uid": 0, "github_uid": 0} +not_updateable_cols_internal = ["email", "createdAt", "expireAt"] +# Columns that can leave EZAuth but should only be used internally can be defined in config diff --git a/src/tools/testing_config.json b/src/tools/conf/testing_config.json similarity index 100% rename from src/tools/testing_config.json rename to src/tools/conf/testing_config.json diff --git a/src/tools/db.py b/src/tools/db.py index f224d24..87087f2 100644 --- a/src/tools/db.py +++ b/src/tools/db.py @@ -2,7 +2,7 @@ import os import bson.json_util import json -from tools.conf import SessionConfig +from .conf import SessionConfig import redis import logging import sys diff --git a/src/tools/mail.py b/src/tools/mail.py index 5c38cac..8ed46ef 100644 --- a/src/tools/mail.py +++ b/src/tools/mail.py @@ -1,7 +1,7 @@ import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from . import EmailConfig +from .conf import EmailConfig import logging from threading import Lock from tools import users_collection, InternalConfig diff --git a/test.py b/test.py new file mode 100644 index 0000000..360a841 --- /dev/null +++ b/test.py @@ -0,0 +1,10 @@ +class Sos: + saus: bool + + def validate(self): + if type(self.saus) != bool: + raise ValueError("Sausage must be a boolean") + + +Sos.saus = True +Sos().validate() From ff685d0082bd6e298d6a1b45132f3ea1ace3ab75 Mon Sep 17 00:00:00 2001 From: John Grubba Date: Fri, 26 Jul 2024 08:39:04 +0200 Subject: [PATCH 2/6] Linting Errors + Restructuring Problems --- src/tools/__init__.py | 11 ++++++++++- src/tools/conf/SignupConfig.py | 4 ++-- test.py | 10 ---------- 3 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 test.py diff --git a/src/tools/__init__.py b/src/tools/__init__.py index e27e61c..aa809d7 100644 --- a/src/tools/__init__.py +++ b/src/tools/__init__.py @@ -1,5 +1,14 @@ from .db import users_collection, sessions_collection, bson_to_json, r -from .conf import * +from .conf import ( + default_signup_fields, + insecure_cols, + SecurityConfig, + AccountFeaturesConfig, + InternalConfig, + SessionConfig, + EmailConfig, + SignupConfig, +) from .mail import send_email, broadcast_emails from .confirmation_codes import all_ids, regenerate_ids diff --git a/src/tools/conf/SignupConfig.py b/src/tools/conf/SignupConfig.py index 95f25ee..473b2df 100644 --- a/src/tools/conf/SignupConfig.py +++ b/src/tools/conf/SignupConfig.py @@ -19,13 +19,13 @@ def validate(self) -> bool: Returns: bool: _description_ """ - if type(self.enable_conf_email) != bool: + if isinstance(self.enable_conf_email, bool): raise ValueError( "signup.enable_conf_email must be a boolean (got type {})".format( type(self.enable_conf_email) ) ) - if type(self.conf_code_expiry) != int: + if isinstance(self.conf_code_expiry, int): raise ValueError( "signup.conf_code_expiry must be an integer (got type {})".format( type(self.conf_code_expiry) diff --git a/test.py b/test.py deleted file mode 100644 index 360a841..0000000 --- a/test.py +++ /dev/null @@ -1,10 +0,0 @@ -class Sos: - saus: bool - - def validate(self): - if type(self.saus) != bool: - raise ValueError("Sausage must be a boolean") - - -Sos.saus = True -Sos().validate() From 1055a32563c166d7818b911c49673732a56e6c03 Mon Sep 17 00:00:00 2001 From: John Grubba Date: Fri, 26 Jul 2024 08:55:19 +0200 Subject: [PATCH 3/6] SignUp Config Validation --- src/api/oauth_providers/github.py | 7 ++++- src/api/oauth_providers/google.py | 23 +++++++++------- src/tools/conf/SignupConfig.py | 45 ++++++++++++++++++++++--------- 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/api/oauth_providers/github.py b/src/api/oauth_providers/github.py index 6312c9c..4d7dd47 100644 --- a/src/api/oauth_providers/github.py +++ b/src/api/oauth_providers/github.py @@ -13,7 +13,12 @@ from crud.sessions import create_login_session from api.model import LoginResponse -github_cnf = json.load(open("/src/app/config/github_client_secret.env.json")) +try: + github_cnf = json.load(open("/src/app/config/github_client_secret.env.json")) +except FileNotFoundError: + raise FileNotFoundError( + "GitHub OAuth Config File not found (github_client_secret.env.json). Please disable this OAuth Provider, or create the file as described in the Docs." + ) REDIRECT_URI = SignupConfig.oauth_base_url + "/oauth/github/callback" CLIENT_ID = github_cnf["client_id"] CLIENT_SECRET = github_cnf["client_secret"] diff --git a/src/api/oauth_providers/google.py b/src/api/oauth_providers/google.py index c9bd60f..44524c2 100644 --- a/src/api/oauth_providers/google.py +++ b/src/api/oauth_providers/google.py @@ -23,15 +23,20 @@ ) # Initialize Googles OAuth Flow -flow = Flow.from_client_secrets_file( - client_secrets_file="/src/app/config/google_client_secret.env.json", - scopes=[ - "https://www.googleapis.com/auth/userinfo.email", - "openid", - "https://www.googleapis.com/auth/userinfo.profile", - ], - redirect_uri=SignupConfig.oauth_base_url + "/oauth/google/callback", -) +try: + flow = Flow.from_client_secrets_file( + client_secrets_file="/src/app/config/google_client_secret.env.json", + scopes=[ + "https://www.googleapis.com/auth/userinfo.email", + "openid", + "https://www.googleapis.com/auth/userinfo.profile", + ], + redirect_uri=SignupConfig.oauth_base_url + "/oauth/google/callback", + ) +except FileNotFoundError: + raise FileNotFoundError( + "Google OAuth Config File not found (google_client_secret.env.json). Please disable this OAuth Provider, or create the file as described in the Docs." + ) @router.get("/login") diff --git a/src/tools/conf/SignupConfig.py b/src/tools/conf/SignupConfig.py index 473b2df..b8c8ab4 100644 --- a/src/tools/conf/SignupConfig.py +++ b/src/tools/conf/SignupConfig.py @@ -9,26 +9,47 @@ class SignupConfig: oauth_providers: list[str] = config["signup"]["oauth"]["providers_enabled"] oauth_base_url: str = str(config["signup"]["oauth"]["base_url"]).removesuffix("/") - def validate(self) -> bool: - """This is to Type Check the Configuration - - Raises: - ValueError: _description_ - ValueError: _description_ - - Returns: - bool: _description_ - """ - if isinstance(self.enable_conf_email, bool): + def validate_types(self) -> bool: + """This is to Type Check the Configuration""" + if not isinstance(self.enable_conf_email, bool): raise ValueError( "signup.enable_conf_email must be a boolean (got type {})".format( type(self.enable_conf_email) ) ) - if isinstance(self.conf_code_expiry, int): + if not isinstance(self.conf_code_expiry, int): raise ValueError( "signup.conf_code_expiry must be an integer (got type {})".format( type(self.conf_code_expiry) ) ) + if not isinstance(self.conf_code_complexity, int): + raise ValueError( + "signup.conf_code_complexity must be an integer (got type {})".format( + type(self.conf_code_complexity) + ) + ) + if not isinstance(self.enable_welcome_email, bool): + raise ValueError( + "signup.enable_welcome_email must be a boolean (got type {})".format( + type(self.enable_welcome_email) + ) + ) + if not isinstance(self.oauth_providers, list): + raise ValueError( + "signup.oauth.providers_enabled must be a list (got type {})".format( + type(self.oauth_providers) + ) + ) + if not all(isinstance(i, str) for i in self.oauth_providers): + raise ValueError("signup.oauth.providers_enabled must be a list of strings") + if not isinstance(self.oauth_base_url, str): + raise ValueError( + "signup.oauth.base_url must be a string (got type {})".format( + type(self.oauth_base_url) + ) + ) pass + + +SignupConfig().validate_types() From 566755636e58ab3882aab1dd9f8ca40cc2138d2f Mon Sep 17 00:00:00 2001 From: John Grubba Date: Fri, 26 Jul 2024 09:15:49 +0200 Subject: [PATCH 4/6] Config Validation --- src/tools/conf/AccountFeaturesConfig.py | 70 +++++++++++++++++++++++++ src/tools/conf/EmailConfig.py | 36 +++++++++++++ src/tools/conf/InternalConfig.py | 29 +++++++++- src/tools/conf/SecurityConfig.py | 42 ++++++++++++++- src/tools/conf/SessionConfig.py | 42 +++++++++++++++ src/tools/conf/SignupConfig.py | 4 +- src/tools/db.py | 2 + 7 files changed, 219 insertions(+), 6 deletions(-) diff --git a/src/tools/conf/AccountFeaturesConfig.py b/src/tools/conf/AccountFeaturesConfig.py index 79033ed..4696932 100644 --- a/src/tools/conf/AccountFeaturesConfig.py +++ b/src/tools/conf/AccountFeaturesConfig.py @@ -8,9 +8,25 @@ class AccountFeaturesConfig: issuer_name_2fa: str = config["account_features"]["2fa"]["issuer_name"] issuer_image_url_2fa: str = config["account_features"]["2fa"]["issuer_image_url"] qr_code_endpoint_2fa: bool = config["account_features"]["2fa"]["qr_endpoint"] + + if not isinstance(config["account_features"]["allow_add_fields_on_signup"], list): + raise ValueError( + "account_features.allow_add_fields_on_signup must be a list (got type {})".format( + type(config["account_features"]["allow_add_fields_on_signup"]) + ) + ) + allow_add_fields_on_signup: set[str] = set( config["account_features"]["allow_add_fields_on_signup"] ) - set(not_updateable_cols_internal) + + if not isinstance(config["account_features"]["allow_add_fields_patch_user"], list): + raise ValueError( + "account_features.allow_add_fields_patch_user must be a list (got type {})".format( + type(config["account_features"]["allow_add_fields_patch_user"]) + ) + ) + allow_add_fields_patch_user: set[str] = set( config["account_features"]["allow_add_fields_patch_user"] ) - set(not_updateable_cols_internal) @@ -18,3 +34,57 @@ class AccountFeaturesConfig: deletion_pending_minutes: int = config["account_features"][ "deletion_pending_minutes" ] + + def validate_types(self) -> bool: + """This is to Type Check the Configuration""" + if not isinstance(self.enable_reset_pswd, bool): + raise ValueError( + "account_features.enable_reset_pswd must be a boolean (got type {})".format( + type(self.enable_reset_pswd) + ) + ) + if not isinstance(self.reset_pswd_conf_mail, bool): + raise ValueError( + "account_features.reset_pswd_conf_mail must be a boolean (got type {})".format( + type(self.reset_pswd_conf_mail) + ) + ) + if not isinstance(self.enable_2fa, bool): + raise ValueError( + "account_features.2fa.enable must be a boolean (got type {})".format( + type(self.enable_2fa) + ) + ) + if not isinstance(self.issuer_name_2fa, str): + raise ValueError( + "account_features.2fa.issuer_name must be a string (got type {})".format( + type(self.issuer_name_2fa) + ) + ) + if not isinstance(self.issuer_image_url_2fa, str): + raise ValueError( + "account_features.2fa.issuer_image_url must be a string (got type {})".format( + type(self.issuer_image_url_2fa) + ) + ) + if not isinstance(self.qr_code_endpoint_2fa, bool): + raise ValueError( + "account_features.2fa.qr_endpoint must be a boolean (got type {})".format( + type(self.qr_code_endpoint_2fa) + ) + ) + if not isinstance(self.allow_deletion, bool): + raise ValueError( + "account_features.allow_deletion must be a boolean (got type {})".format( + type(self.allow_deletion) + ) + ) + if not isinstance(self.deletion_pending_minutes, int): + raise ValueError( + "account_features.deletion_pending_minutes must be an integer (got type {})".format( + type(self.deletion_pending_minutes) + ) + ) + + +AccountFeaturesConfig().validate_types() diff --git a/src/tools/conf/EmailConfig.py b/src/tools/conf/EmailConfig.py index 27fb70f..878276b 100644 --- a/src/tools/conf/EmailConfig.py +++ b/src/tools/conf/EmailConfig.py @@ -7,3 +7,39 @@ class EmailConfig: sender_email: str = config["email"]["sender_email"] smtp_host: str = config["email"]["smtp_host"] smtp_port: int = config["email"]["smtp_port"] + + def validate_types(self) -> None: + """This is to Type Check the Configuration""" + if not isinstance(self.login_usr, str): + raise ValueError( + "email.login_usr must be a string (got type {})".format( + type(self.login_usr) + ) + ) + if not isinstance(self.login_pwd, str): + raise ValueError( + "email.login_pwd must be a string (got type {})".format( + type(self.login_pwd) + ) + ) + if not isinstance(self.sender_email, str): + raise ValueError( + "email.sender_email must be a string (got type {})".format( + type(self.sender_email) + ) + ) + if not isinstance(self.smtp_host, str): + raise ValueError( + "email.smtp_host must be a string (got type {})".format( + type(self.smtp_host) + ) + ) + if not isinstance(self.smtp_port, int): + raise ValueError( + "email.smtp_port must be an integer (got type {})".format( + type(self.smtp_port) + ) + ) + + +EmailConfig().validate_types() diff --git a/src/tools/conf/InternalConfig.py b/src/tools/conf/InternalConfig.py index 9f496b5..a96f7be 100644 --- a/src/tools/conf/InternalConfig.py +++ b/src/tools/conf/InternalConfig.py @@ -4,13 +4,38 @@ class InternalConfig: internal_api_key: str = config["internal"]["internal_api_key"] + # Type Check here because calculations need to be done immediately + if not isinstance(config["internal"]["internal_columns"], list): + raise ValueError( + "internal.internal_columns must be a list (got type {})".format( + type(config["internal"]["internal_columns"]) + ) + ) internal_columns: dict = dict( - ChainMap(*[{col: 0} for col in config["internal"]["internal_columns"]]) + ChainMap(*[{col: 0} for col in set(config["internal"]["internal_columns"])]) ) internal_columns.update(insecure_cols) # Insecure Cols + Internal Cols can't be updated by the user - not_updateable_columns: list = ( + if not isinstance(config["internal"]["not_updateable_columns"], list): + raise ValueError( + "internal.not_updateable_columns must be a list (got type {})".format( + type(config["internal"]["not_updateable_columns"]) + ) + ) + not_updateable_columns: set = set( config["internal"]["not_updateable_columns"] + list(internal_columns.keys()) + not_updateable_cols_internal ) + + def validate_types(self) -> bool: + """This is to Type Check the Configuration""" + if not isinstance(self.internal_api_key, str): + raise ValueError( + "internal.internal_api_key must be a string (got type {})".format( + type(self.internal_api_key) + ) + ) + + +InternalConfig().validate_types() diff --git a/src/tools/conf/SecurityConfig.py b/src/tools/conf/SecurityConfig.py index 6aa61c7..4cb0204 100644 --- a/src/tools/conf/SecurityConfig.py +++ b/src/tools/conf/SecurityConfig.py @@ -2,8 +2,46 @@ class SecurityConfig: - access_control_origins: set[str] = set(config["security"]["allow_origins"]) - allow_headers: set[str] = set(config["security"]["allow_headers"]) + access_control_origins: set[str] = config["security"]["allow_origins"] + allow_headers: set[str] = config["security"]["allow_headers"] max_login_attempts: int = config["security"]["max_login_attempts"] login_timeout: int = config["security"]["login_timeout"] expire_unfinished_timeout: int = config["security"]["expire_unfinished_timeout"] + + def validate_types(self) -> bool: + """This is to Type Check the Configuration""" + if not isinstance(self.access_control_origins, list): + self.access_control_origins = set(self.access_control_origins) + raise ValueError( + "security.allow_origins must be a list (got type {})".format( + type(self.access_control_origins) + ) + ) + if not isinstance(self.allow_headers, list): + self.allow_headers = set(self.allow_headers) + raise ValueError( + "security.allow_headers must be a list (got type {})".format( + type(self.allow_headers) + ) + ) + if not isinstance(self.max_login_attempts, int): + raise ValueError( + "security.max_login_attempts must be an integer (got type {})".format( + type(self.max_login_attempts) + ) + ) + if not isinstance(self.login_timeout, int): + raise ValueError( + "security.login_timeout must be an integer (got type {})".format( + type(self.login_timeout) + ) + ) + if not isinstance(self.expire_unfinished_timeout, int): + raise ValueError( + "security.expire_unfinished_timeout must be an integer (got type {})".format( + type(self.expire_unfinished_timeout) + ) + ) + + +SecurityConfig().validate_types() diff --git a/src/tools/conf/SessionConfig.py b/src/tools/conf/SessionConfig.py index 85624da..dd13ff6 100644 --- a/src/tools/conf/SessionConfig.py +++ b/src/tools/conf/SessionConfig.py @@ -8,3 +8,45 @@ class SessionConfig: auto_cookie_name: str = config["session"]["auto_cookie_name"] cookie_samesite: str = config["session"]["cookie_samesite"] cookie_secure: bool = config["session"]["cookie_secure"] + + def validate_types(self) -> bool: + """This is to Type Check the Configuration""" + if not isinstance(self.session_expiry_seconds, int): + raise ValueError( + "session.session_expiry_seconds must be an integer (got type {})".format( + type(self.session_expiry_seconds) + ) + ) + if not isinstance(self.max_session_count, int): + raise ValueError( + "session.max_session_count must be an integer (got type {})".format( + type(self.max_session_count) + ) + ) + if not isinstance(self.auto_cookie, bool): + raise ValueError( + "session.auto_cookie must be a boolean (got type {})".format( + type(self.auto_cookie) + ) + ) + if not isinstance(self.auto_cookie_name, str): + raise ValueError( + "session.auto_cookie_name must be a string (got type {})".format( + type(self.auto_cookie_name) + ) + ) + if not isinstance(self.cookie_samesite, str): + raise ValueError( + "session.cookie_samesite must be a string (got type {})".format( + type(self.cookie_samesite) + ) + ) + if not isinstance(self.cookie_secure, bool): + raise ValueError( + "session.cookie_secure must be a boolean (got type {})".format( + type(self.cookie_secure) + ) + ) + + +SessionConfig().validate_types() diff --git a/src/tools/conf/SignupConfig.py b/src/tools/conf/SignupConfig.py index b8c8ab4..680cd85 100644 --- a/src/tools/conf/SignupConfig.py +++ b/src/tools/conf/SignupConfig.py @@ -6,7 +6,7 @@ class SignupConfig: conf_code_expiry: int = config["signup"]["conf_code_expiry"] conf_code_complexity: int = config["signup"]["conf_code_complexity"] enable_welcome_email: bool = config["signup"]["enable_welcome_email"] - oauth_providers: list[str] = config["signup"]["oauth"]["providers_enabled"] + oauth_providers: set[str] = config["signup"]["oauth"]["providers_enabled"] oauth_base_url: str = str(config["signup"]["oauth"]["base_url"]).removesuffix("/") def validate_types(self) -> bool: @@ -36,6 +36,7 @@ def validate_types(self) -> bool: ) ) if not isinstance(self.oauth_providers, list): + self.oauth_providers = set(self.oauth_providers) raise ValueError( "signup.oauth.providers_enabled must be a list (got type {})".format( type(self.oauth_providers) @@ -49,7 +50,6 @@ def validate_types(self) -> bool: type(self.oauth_base_url) ) ) - pass SignupConfig().validate_types() diff --git a/src/tools/db.py b/src/tools/db.py index 87087f2..0fe1a46 100644 --- a/src/tools/db.py +++ b/src/tools/db.py @@ -54,6 +54,8 @@ # Create TTL For Account Deletions users_collection.create_index("expiresAfter", expireAfterSeconds=0, sparse=True) +logger.info("\u001b[32m+ MongoDB Setup Done\u001b[0m") + def bson_to_json(data: bson.BSON) -> dict: """Convert BSON to JSON. From MongoDB to JSON. From 7dc9d598708f5ca4afb81e8eb5b3419606e31af4 Mon Sep 17 00:00:00 2001 From: John Grubba Date: Fri, 26 Jul 2024 09:16:41 +0200 Subject: [PATCH 5/6] Fix Line too Long error --- src/api/oauth_providers/github.py | 3 ++- src/api/oauth_providers/google.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/oauth_providers/github.py b/src/api/oauth_providers/github.py index 4d7dd47..0062563 100644 --- a/src/api/oauth_providers/github.py +++ b/src/api/oauth_providers/github.py @@ -17,7 +17,8 @@ github_cnf = json.load(open("/src/app/config/github_client_secret.env.json")) except FileNotFoundError: raise FileNotFoundError( - "GitHub OAuth Config File not found (github_client_secret.env.json). Please disable this OAuth Provider, or create the file as described in the Docs." + "GitHub OAuth Config File not found (github_client_secret.env.json).\ + Please disable this OAuth Provider, or create the file as described in the Docs." ) REDIRECT_URI = SignupConfig.oauth_base_url + "/oauth/github/callback" CLIENT_ID = github_cnf["client_id"] diff --git a/src/api/oauth_providers/google.py b/src/api/oauth_providers/google.py index 44524c2..839e21a 100644 --- a/src/api/oauth_providers/google.py +++ b/src/api/oauth_providers/google.py @@ -35,7 +35,8 @@ ) except FileNotFoundError: raise FileNotFoundError( - "Google OAuth Config File not found (google_client_secret.env.json). Please disable this OAuth Provider, or create the file as described in the Docs." + "Google OAuth Config File not found (google_client_secret.env.json).\ + Please disable this OAuth Provider, or create the file as described in the Docs." ) From 063fa1a3f54394c8c454c94cd209ca588837a519 Mon Sep 17 00:00:00 2001 From: John Grubba Date: Fri, 26 Jul 2024 09:22:03 +0200 Subject: [PATCH 6/6] Username and Password Complexity Updated --- src/api/model.py | 11 ++++++++--- src/tools/conf/SignupConfig.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/api/model.py b/src/api/model.py index 8381b18..dc82e60 100644 --- a/src/api/model.py +++ b/src/api/model.py @@ -65,7 +65,10 @@ def password_check_hash(cls, password: SecretStr) -> str: raise ValueError("Make sure your password has a number in it") elif re.search("[A-Z]", pswd) is None and SignupConfig.password_complexity >= 3: raise ValueError("Make sure your password has a capital letter in it") - elif re.search("[^a-zA-Z0-9]", pswd) is None and SignupConfig.password_complexity >= 4: + elif ( + re.search("[^a-zA-Z0-9]", pswd) is None + and SignupConfig.password_complexity >= 4 + ): raise ValueError("Make sure your password has a special character in it") elif len(pswd) > 50: raise ValueError("Make sure your password is at most 50 characters") @@ -98,9 +101,11 @@ def username_check(cls, username: str) -> str: if len(username) == 0: raise ValueError("Username cannot be empty") if len(username) < 4: - if SignupConfig.username_complexity >= 1: raise ValueError("Username must be at least 4 characters long") + if SignupConfig.username_complexity >= 1: + raise ValueError("Username must be at least 4 characters long") if len(username) > 20: - if SignupConfig.username_complexity >= 2: raise ValueError("Username must be at most 20 characters long") + if SignupConfig.username_complexity >= 2: + raise ValueError("Username must be at most 20 characters long") elif re.search("[^a-zA-Z0-9]", username) is not None: raise ValueError("Username must only contain letters and numbers") return username diff --git a/src/tools/conf/SignupConfig.py b/src/tools/conf/SignupConfig.py index 680cd85..1798d83 100644 --- a/src/tools/conf/SignupConfig.py +++ b/src/tools/conf/SignupConfig.py @@ -8,6 +8,8 @@ class SignupConfig: enable_welcome_email: bool = config["signup"]["enable_welcome_email"] oauth_providers: set[str] = config["signup"]["oauth"]["providers_enabled"] oauth_base_url: str = str(config["signup"]["oauth"]["base_url"]).removesuffix("/") + password_complexity: int = config["signup"]["password_complexity"] + username_complexity: int = config["signup"]["username_complexity"] def validate_types(self) -> bool: """This is to Type Check the Configuration""" @@ -50,6 +52,18 @@ def validate_types(self) -> bool: type(self.oauth_base_url) ) ) + if not isinstance(self.password_complexity, int): + raise ValueError( + "signup.password_complexity must be an integer (got type {})".format( + type(self.password_complexity) + ) + ) + if not isinstance(self.username_complexity, int): + raise ValueError( + "signup.username_complexity must be an integer (got type {})".format( + type(self.username_complexity) + ) + ) SignupConfig().validate_types()