""" AUTHOR: Khushal P Soonderji DATE: ORIGINAL: Monday, 2nd Dec., 2024 UPGRADED: Monday, 9th Dec., 2024 OBJECTIVE: To define the interaction between the UI layer and the database connectivity in one place. Here we shall handle all the activities for OAuth2.0 authorization requests for all the users of our service. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # My async utils: from utils_v2.string import json from utils_v2.date_time import date_time from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mongo_v2 import AsyncMongo # Base model: from models.behaviour.base import BaseModel # Data models: from models.data.core.auth_token import CoreAuthTokenModel # To work with MongoDB: from bson import ObjectId # To work with datatypes: from typing import Literal # To make deep-copies: import copy # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class MailOAuthModel(BaseModel): AUTH_COLLECTION = "_authTokens" async def get_token_id( self, db_conn: AsyncMySQL, mongo_conn: AsyncMongo, auth_token: CoreAuthTokenModel, session_token: str = None ) -> ObjectId: """ Stores params from the session info and gives an identifier to use in the authorization URL. Use this when the user requests an authorization URL to link your service to another service (like GMail). :param db_conn: The database connection (MariaDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action. :param auth_token: An instance of the core auth-token model that holds data in the database. :param session_token: The session token of the user who requested this service. :return: An ObjectId to later store the granted tokens. """ # Note down the timestamp at which this event occurred: request_ts = date_time.get_current_utc_date_time(as_string = False) # Get the identifier from the database. # BE CAREFUL WITH THE KEYS HERE, THEY SHOULD MATCH THE FIELDS OF THE CORE AUTH-TOKEN MODEL: mongo_json = await mongo_conn.find_one_and_update( collection = MailOAuthModel.AUTH_COLLECTION, filter = mongo_conn.dict_to_dot_notation({ "serviceType": auth_token.serviceType, "user": { "entityId": auth_token.user.entityId, "billingAccountId": auth_token.user.billingAccountId }, "clientUserId": auth_token.clientUserId }), update = { "$set": { "lastRequestTs": auth_token.lastRequestTs, "status": auth_token.status, "syncFreq": auth_token.syncFreq }, "$setOnInsert": { "version": auth_token.version, "serviceType": auth_token.serviceType, "client": auth_token.client, "authType": auth_token.authType, "user": auth_token.user.model_dump(), "clientUserId": auth_token.clientUserId, "auth": auth_token.auth, "token": auth_token.token, "firstRefreshTs": auth_token.firstRefreshTs, "lastRefreshTs": auth_token.lastRefreshTs, "firstRequestTs": auth_token.firstRequestTs or request_ts, } }, projection = { "_id": True }, upsert = True, return_updated = True ) # Tell MariaDB that an authorization request was initiated: db_json = {} if mongo_json is not None: db_json = await self.call_procedure( db_conn = db_conn, proc_name = "entity_integration_save", proc_args = ( auth_token.user.entityId, # ......................................... 'p_entity_id' auth_token.client, # ................................................ 'p_provider' auth_token.status, # ................................................ 'p_current_status' "Auth Requested", # ................................................. 'p_last_action' None, # ............................................................. 'p_display_name' None, # ............................................................. 'p_display_picture' str(mongo_json["_id"]), # ........................................... 'p_token_id' json.to_string(python_data = {"email": None}, no_space = True), # ... 'p_notes' auth_token.user.userId # ............................................ 'p_created_by' ), session_token = session_token ) # Done here: return mongo_json["_id"] if mongo_json and db_json.get("status") == 1 else None async def set_token( self, db_conn: AsyncMySQL, mongo_conn: AsyncMongo, token_id: ObjectId | str, auth_token: CoreAuthTokenModel, session_token: str = None ) -> bool: """ This method is to be called when the end user authorizes your service to connect to his third-party account. For example, when the end user allows you to access his GMail account. USE THIS FOR UPDATING (REFRESHING) TOKENS ALSO. :param db_conn: The database connection (MariaDB) to use to perform the action. :param mongo_conn: The database connection (MongoDB) to use to perform the action. :param token_id: The identifier granted by the 'get_token_id' method. :param auth_token: The actual auth/token data to be saved to the database. :param session_token: The session token of the user who requested this service. :return: True if saved, False if failed. """ # Start by assuming failure: token_saved = False # Note down the timestamp at which this event occurred: request_ts = date_time.get_current_utc_date_time(as_string = False) # Save the token to MongoDB. # BE CAREFUL WITH THE KEYS HERE, THEY SHOULD MATCH THE FIELDS OF THE CORE AUTH-TOKEN MODEL: mongo_json = await mongo_conn.find_one_and_update( collection = MailOAuthModel.AUTH_COLLECTION, filter = mongo_conn.dict_to_dot_notation({ "_id": ObjectId(token_id), "clientUserId": auth_token.clientUserId }), update = [{ "$set": { "token": auth_token.token, "status": auth_token.status, "lastRefreshTs": request_ts, "firstRefreshTs": { "$cond": { "if": { "$or": [ {"$eq": ["$firstRefreshTs", None]}, {"$eq": [{"$type": "$firstRefreshTs"}, "missing"]} ] }, "then": request_ts, "else": "$firstRefreshTs" } } } }], projection = {"token": False}, return_updated = True, upsert = False ) # Tell MariaDB that the token was saved: if mongo_json is not None: token_notes = { "email": auth_token.token["email"], "displayName": auth_token.token.get("displayName"), "displayPictureUrl": auth_token.token.get("displayPictureUrl"), } db_json = await self.call_procedure( db_conn = db_conn, proc_name = "entity_integration_save", proc_args = ( mongo_json["user"]["entityId"], # ............................... 'p_entity_id' mongo_json["client"], # ......................................... 'p_provider' auth_token.status, # ............................................ 'p_current_status' "Auth Granted", # ............................................... 'p_last_action' auth_token.token.get("displayName"), # .......................... 'p_display_name' auth_token.token.get("displayPictureUrl"), # .................... 'p_display_picture' token_id, # ..................................................... 'p_token_id' json.to_string(python_data = token_notes, no_space = True), # ... 'p_notes' auth_token.user.userId # ........................................ 'p_created_by' ), session_token = session_token ) if db_json["status"] == 1: token_saved = True # Done here: return token_saved async def get_token( self, mongo_conn: AsyncMongo, token_id: ObjectId | str = None, **kwargs ) -> CoreAuthTokenModel | None: """ To retrieve stored tokens from the database. :param mongo_conn: The database connection (MongoDB) to use to perform the action. :param token_id: The identifier granted by the 'get_token_id' method. :param kwargs: Any set of key-value pairs to build custom search criteria. This could be things like the user info, the client, the type of authentication used, or even the kind of service. :return: The retrieved record that has the token, and information about the service and client if found, else None when there is no matching record. """ # Build the filter: filter_json = {k: v for k, v in kwargs.items()} if token_id: filter_json["_id"] = ObjectId(token_id) # If there is no search criteria, we exit with failure: if not filter_json: return None # If there is some filtering possible, we fetch the token: token = await mongo_conn.find_one( collection = self.AUTH_COLLECTION, filter = filter_json, ) # Done here: return CoreAuthTokenModel(**token) if token else None # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass