diff --git a/modules/agentEmail.py b/modules/agentEmail.py index 0e64dd70..3c843ddc 100644 --- a/modules/agentEmail.py +++ b/modules/agentEmail.py @@ -230,28 +230,19 @@ class AgentEmail(AgentBase): # Add document name to contents documentContents.append(f"\n\n--- {docName} ---\n") - # Process contents - hasAttachment = False - for content in doc.get("contents", []): - # Add extracted text to document contents - if content.get("dataExtracted"): - documentContents.append(content.get("dataExtracted", "")) - - # Prepare attachment if it has content data - if content.get("data"): - # Check if this content should be an attachment - # Typically files like PDFs, images, etc. - contentType = content.get("contentType", "") - if (not contentType.startswith("text/") or - contentType in ["application/pdf", "application/msword"]): - hasAttachment = True - - # If document has content to attach, add to attachments - if hasAttachment: + # Check if document has data to attach + if doc.get("data"): + # Add to attachments attachments.append({ "name": docName, "document": doc }) + + # Add document name to contents + documentContents.append(f"Document attached: {docName}") + else: + # If no data, just add the name + documentContents.append(f"Document referenced: {docName}") return "\n".join(documentContents), attachments @@ -481,7 +472,7 @@ class AgentEmail(AgentBase): def _createGraphDraftEmail(self, access_token, recipient, subject, body, attachments=None): """ Create a draft email using Microsoft Graph API with fixed attachment handling. - Uses the complete document data for attachments. + Uses the document data directly for attachments. Args: access_token: Microsoft Graph access token @@ -524,13 +515,13 @@ class AgentEmail(AgentBase): logger.info(f"Processing attachment: {file_name}") - # Get the document data + # Get the document data directly file_content = doc.get('data') if not file_content: logger.warning(f"No data found for attachment: {file_name}") continue - # Get content type from document + # Get content type and base64 flag content_type = doc.get('contentType', 'application/octet-stream') is_base64 = doc.get('base64Encoded', False) @@ -556,18 +547,21 @@ class AgentEmail(AgentBase): '@odata.type': '#microsoft.graph.fileAttachment', 'name': file_name, 'contentType': content_type, - 'contentBytes': file_content + 'contentBytes': file_content, + 'isInline': False, + 'size': len(base64.b64decode(file_content)) if file_content else 0 } email_data['attachments'].append(attachment_data) logger.info(f"Successfully added attachment: {file_name}") - # Try to create draft using drafts folder endpoint (Option 1) + # Try to create draft using drafts folder endpoint try: logger.info("Attempting to create draft email using drafts folder endpoint") logger.info(f"Email data structure: subject={subject}, recipient={recipient}, " + - f"has_attachments={bool(email_data.get('attachments'))}, " + - f"attachment_count={len(email_data.get('attachments', []))}") + f"has_attachments={bool(email_data.get('attachments'))}, " + + f"attachment_count={len(email_data.get('attachments', []))}") + # First create the draft message response = requests.post( 'https://graph.microsoft.com/v1.0/me/mailFolders/drafts/messages', headers=headers, @@ -580,7 +574,7 @@ class AgentEmail(AgentBase): else: logger.error(f"Drafts folder method failed: {response.status_code} - {response.text}") - # Try fallback method with messages endpoint (Option 2) + # Try fallback method with messages endpoint logger.info("Trying fallback with messages endpoint") response = requests.post( 'https://graph.microsoft.com/v1.0/me/messages', diff --git a/modules/gatewayInterface.py b/modules/gatewayInterface.py index 94359949..ea5aa542 100644 --- a/modules/gatewayInterface.py +++ b/modules/gatewayInterface.py @@ -393,7 +393,11 @@ class GatewayInterface: def authenticateUser(self, username: str, password: str) -> Optional[Dict[str, Any]]: """Authenticates a user by username and password.""" - # Instead of using UAM filtering, directly get user from database + # Clear the users table from cache and reload it + if "users" in self.db._tablesCache: + del self.db._tablesCache["users"] + + # Get fresh user data users = self.db.getRecordset("users") user = next((u for u in users if u.get("username") == username), None) diff --git a/static/37_gatewayInterface.py b/static/37_gatewayInterface.py new file mode 100644 index 00000000..94359949 --- /dev/null +++ b/static/37_gatewayInterface.py @@ -0,0 +1,522 @@ +""" +Interface to the Gateway system. +Manages users and mandates for authentication. +""" + +import os +import logging +from typing import Dict, Any, List, Optional, Union +import importlib +from passlib.context import CryptContext + +from connectors.connectorDbJson import DatabaseConnector +from modules.configuration import APP_CONFIG + +logger = logging.getLogger(__name__) + +# Password-Hashing +pwdContext = CryptContext(schemes=["argon2"], deprecated="auto") + + +class GatewayInterface: + """ + Interface to the Gateway system. + Manages users and mandates. + """ + + def __init__(self, mandateId: int = None, userId: int = None): + """Initializes the Gateway Interface with optional mandate and user context.""" + # Context can be empty during initialization + self.mandateId = mandateId + self.userId = userId + + # Import data model module + try: + self.modelModule = importlib.import_module("modules.gatewayModel") + logger.info("gatewayModel successfully imported") + except ImportError as e: + logger.error(f"Error importing gatewayModel: {e}") + raise + + # Initialize database + self._initializeDatabase() + + # Load user information + self.currentUser = self._getCurrentUserInfo() + + # Initialize standard records if needed + self._initRecords() + + def _getCurrentUserInfo(self) -> Dict[str, Any]: + """Gets information about the current user including privileges.""" + # For initialization, set default values + userInfo = { + "id": self.userId, + "mandateId": self.mandateId, + "privilege": "user", # Default privilege level + "language": "en" + } + + # Try to load actual user info if IDs are provided + if self.userId: + userRecords = self.db.getRecordset("users", recordFilter={"id": self.userId}) + if userRecords: + user = userRecords[0] + userInfo["privilege"] = user.get("privilege", "user") + userInfo["language"] = user.get("language", "en") + + return userInfo + + def _initializeDatabase(self): + """Initializes the database connection.""" + self.db = DatabaseConnector( + dbHost=APP_CONFIG.get("DB_SYSTEM_HOST"), + dbDatabase=APP_CONFIG.get("DB_SYSTEM_DATABASE"), + dbUser=APP_CONFIG.get("DB_SYSTEM_USER"), + dbPassword=APP_CONFIG.get("DB_SYSTEM_PASSWORD_SECRET"), + mandateId=self.mandateId if self.mandateId else 0, + userId=self.userId if self.userId else 0 + ) + + def _initRecords(self): + """Initializes standard records in the database if they don't exist.""" + self._initRootMandate() + self._initAdminUser() + + def _initRootMandate(self): + """Creates the Root mandate if it doesn't exist.""" + existingMandateId = self.getInitialId("mandates") + mandates = self.db.getRecordset("mandates") + if existingMandateId is None or not mandates: + logger.info("Creating Root mandate") + rootMandate = { + "name": "Root", + "language": "de" + } + createdMandate = self.db.recordCreate("mandates", rootMandate) + logger.info(f"Root mandate created with ID {createdMandate['id']}") + + # Update mandate context + self.mandateId = createdMandate['id'] + + def _initAdminUser(self): + """Creates the Admin user if it doesn't exist.""" + existingUserId = self.getInitialId("users") + users = self.db.getRecordset("users") + if existingUserId is None or not users: + logger.info("Creating Admin user") + adminUser = { + "mandateId": self.mandateId, + "username": "admin", + "email": "admin@example.com", + "fullName": "Administrator", + "disabled": False, + "language": "de", + "privilege": "sysadmin", + "hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production! + } + createdUser = self.db.recordCreate("users", adminUser) + logger.info(f"Admin user created with ID {createdUser['id']}") + + # Update user context + self.userId = createdUser['id'] + + def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + Unified user access management function that filters data based on user privileges. + + Args: + table: Name of the table + recordset: Recordset to filter based on access rules + + Returns: + Filtered recordset based on user privilege level + """ + userPrivilege = self.currentUser.get("privilege", "user") + + # Apply filtering based on privilege + if userPrivilege == "sysadmin": + return recordset # System admins see all records + elif userPrivilege == "admin": + # Admins see records in their mandate + return [r for r in recordset if r.get("mandateId") == self.mandateId] + else: # Regular users + # Users only see records they own within their mandate + return [r for r in recordset + if r.get("mandateId") == self.mandateId and r.get("userId") == self.userId] + + def _canModify(self, table: str, recordId: Optional[int] = None) -> bool: + """ + Checks if the current user can modify (create/update/delete) records in a table. + + Args: + table: Name of the table + recordId: Optional record ID for specific record check + + Returns: + Boolean indicating permission + """ + userPrivilege = self.currentUser.get("privilege", "user") + + # System admins can modify anything + if userPrivilege == "sysadmin": + return True + + # Check specific record permissions + if recordId is not None: + # Get the record to check ownership + records = self.db.getRecordset(table, recordFilter={"id": recordId}) + if not records: + return False + + record = records[0] + + # Admins can modify anything in their mandate + if userPrivilege == "admin" and record.get("mandateId") == self.mandateId: + # Exception: Can't modify Root mandate unless you are a sysadmin + if table == "mandates" and recordId == 1 and userPrivilege != "sysadmin": + return False + return True + + # Users can only modify their own records + if (record.get("mandateId") == self.mandateId and + record.get("userId") == self.userId): + return True + + return False + else: + # For general table modify permission (e.g., create) + # Admins can create anything in their mandate + if userPrivilege == "admin": + return True + + # Regular users can create most entities + if table == "mandates": + return False # Regular users can't create mandates + return True + + def getInitialId(self, table: str) -> Optional[int]: + """Returns the initial ID for a table.""" + return self.db.getInitialId(table) + + def _getPasswordHash(self, password: str) -> str: + """Creates a hash for a password.""" + return pwdContext.hash(password) + + def _verifyPassword(self, plainPassword: str, hashedPassword: str) -> bool: + """Checks if the password matches the hash.""" + return pwdContext.verify(plainPassword, hashedPassword) + + def _getCurrentTimestamp(self) -> str: + """Returns the current timestamp in ISO format.""" + from datetime import datetime + return datetime.now().isoformat() + + # Mandate methods + + def getAllMandates(self) -> List[Dict[str, Any]]: + """Returns mandates based on user access level.""" + allMandates = self.db.getRecordset("mandates") + return self._uam("mandates", allMandates) + + def getMandate(self, mandateId: int) -> Optional[Dict[str, Any]]: + """Returns a mandate by ID if user has access.""" + mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId}) + if not mandates: + return None + + filteredMandates = self._uam("mandates", mandates) + return filteredMandates[0] if filteredMandates else None + + def createMandate(self, name: str, language: str = "de") -> Dict[str, Any]: + """Creates a new mandate if user has permission.""" + if not self._canModify("mandates"): + raise PermissionError("No permission to create mandates") + + mandateData = { + "name": name, + "language": language + } + + return self.db.recordCreate("mandates", mandateData) + + def updateMandate(self, mandateId: int, mandateData: Dict[str, Any]) -> Dict[str, Any]: + """Updates a mandate if user has access.""" + # Check if the mandate exists and user has access + mandate = self.getMandate(mandateId) + if not mandate: + raise ValueError(f"Mandate with ID {mandateId} not found") + + if not self._canModify("mandates", mandateId): + raise PermissionError(f"No permission to update mandate {mandateId}") + + # Update the mandate + return self.db.recordModify("mandates", mandateId, mandateData) + + def deleteMandate(self, mandateId: int) -> bool: + """ + Deletes a mandate and all associated users and data if user has permission. + """ + # Check if the mandate exists and user has access + mandate = self.getMandate(mandateId) + if not mandate: + return False + + if not self._canModify("mandates", mandateId): + raise PermissionError(f"No permission to delete mandate {mandateId}") + + # Check if it's the initial mandate + initialMandateId = self.getInitialId("mandates") + if initialMandateId is not None and mandateId == initialMandateId: + logger.warning(f"Attempt to delete the Root mandate was prevented") + return False + + # Find all users of the mandate + users = self.getUsersByMandate(mandateId) + + # Delete all users of the mandate and their associated data + for user in users: + self.deleteUser(user["id"]) + + # Delete the mandate + success = self.db.recordDelete("mandates", mandateId) + + if success: + logger.info(f"Mandate with ID {mandateId} was successfully deleted") + else: + logger.error(f"Error deleting mandate with ID {mandateId}") + + return success + + # User methods + + def getAllUsers(self) -> List[Dict[str, Any]]: + """Returns users based on user access level.""" + allUsers = self.db.getRecordset("users") + filteredUsers = self._uam("users", allUsers) + + # Remove password hashes + for user in filteredUsers: + if "hashedPassword" in user: + del user["hashedPassword"] + + return filteredUsers + + def getUsersByMandate(self, mandateId: int) -> List[Dict[str, Any]]: + """Returns users for a specific mandate if user has access.""" + # First check if user has access to the mandate + mandate = self.getMandate(mandateId) + if not mandate: + return [] + + # Get users for this mandate + users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId}) + filteredUsers = self._uam("users", users) + + # Remove password hashes + for user in filteredUsers: + if "hashedPassword" in user: + del user["hashedPassword"] + + return filteredUsers + + def getUserByUsername(self, username: str) -> Optional[Dict[str, Any]]: + """Returns a user by username.""" + users = self.db.getRecordset("users") + for user in users: + if user.get("username") == username: + return user + return None + + def getUser(self, userId: int) -> Optional[Dict[str, Any]]: + """Returns a user by ID if user has access.""" + users = self.db.getRecordset("users", recordFilter={"id": userId}) + if not users: + return None + + filteredUsers = self._uam("users", users) + if not filteredUsers: + return None + + user = filteredUsers[0] + + # Remove password hash + if "hashedPassword" in user: + userCopy = user.copy() + del userCopy["hashedPassword"] + return userCopy + + return user + + def createUser(self, username: str, password: str, email: str = None, + fullName: str = None, language: str = "de", mandateId: int = None, + disabled: bool = False, privilege: str = "user") -> Dict[str, Any]: + """Creates a new user if current user has permission.""" + # Check if the username already exists + existingUser = self.getUserByUsername(username) + if existingUser: + raise ValueError(f"User '{username}' already exists") + + # Use the provided mandateId or the current context + userMandateId = mandateId if mandateId is not None else self.mandateId + + # Check if user has access to the mandate + if userMandateId != self.mandateId and self.currentUser.get("privilege") != "sysadmin": + raise PermissionError(f"No permission to create users in mandate {userMandateId}") + + if not self._canModify("users"): + raise PermissionError("No permission to create users") + + # Check privilege escalation + if (privilege == "sysadmin" or + (privilege == "admin" and self.currentUser.get("privilege") == "user")): + raise PermissionError(f"Cannot create user with higher privilege: {privilege}") + + userData = { + "mandateId": userMandateId, + "username": username, + "email": email, + "fullName": fullName, + "disabled": disabled, + "language": language, + "privilege": privilege, + "hashedPassword": self._getPasswordHash(password) + } + + createdUser = self.db.recordCreate("users", userData) + + # Remove password hash from the response + if "hashedPassword" in createdUser: + del createdUser["hashedPassword"] + + return createdUser + + def authenticateUser(self, username: str, password: str) -> Optional[Dict[str, Any]]: + """Authenticates a user by username and password.""" + # Instead of using UAM filtering, directly get user from database + users = self.db.getRecordset("users") + user = next((u for u in users if u.get("username") == username), None) + + if not user: + return None + + if not self._verifyPassword(password, user.get("hashedPassword", "")): + return None + + # Check if the user is disabled + if user.get("disabled", False): + return None + + # Create a copy without password hash + authenticatedUser = {**user} + if "hashedPassword" in authenticatedUser: + del authenticatedUser["hashedPassword"] + + return authenticatedUser + + + def updateUser(self, userId: int, userData: Dict[str, Any]) -> Dict[str, Any]: + """Updates a user if current user has permission.""" + # Check if the user exists and current user has access + user = self.getUser(userId) + if not user: + # Try to get the raw user record for admin access check + users = self.db.getRecordset("users", recordFilter={"id": userId}) + if not users: + raise ValueError(f"User with ID {userId} not found") + + # Check if current user is admin/sysadmin + if not self._canModify("users", userId): + raise PermissionError(f"No permission to update user {userId}") + + user = users[0] + + # Check privilege escalation + if "privilege" in userData: + currentPrivilege = self.currentUser.get("privilege") + targetPrivilege = userData["privilege"] + + if (targetPrivilege == "sysadmin" and currentPrivilege != "sysadmin") or ( + targetPrivilege == "admin" and currentPrivilege == "user"): + raise PermissionError(f"Cannot escalate privilege to {targetPrivilege}") + + # If the password is being changed, hash it + if "password" in userData: + userData["hashedPassword"] = self._getPasswordHash(userData["password"]) + del userData["password"] + + # Update the user + updatedUser = self.db.recordModify("users", userId, userData) + + # Remove password hash from the response + if "hashedPassword" in updatedUser: + del updatedUser["hashedPassword"] + + return updatedUser + + def disableUser(self, userId: int) -> Dict[str, Any]: + """Disables a user if current user has permission.""" + return self.updateUser(userId, {"disabled": True}) + + def enableUser(self, userId: int) -> Dict[str, Any]: + """Enables a user if current user has permission.""" + return self.updateUser(userId, {"disabled": False}) + + def _deleteUserReferencedData(self, userId: int) -> None: + """Deletes all data associated with a user.""" + # Delete user attributes + try: + attributes = self.db.getRecordset("attributes", recordFilter={"userId": userId}) + for attribute in attributes: + self.db.recordDelete("attributes", attribute["id"]) + except Exception as e: + logger.error(f"Error deleting attributes for user {userId}: {e}") + + logger.info(f"All referenced data for user {userId} has been deleted") + + def deleteUser(self, userId: int) -> bool: + """Deletes a user and all associated data if current user has permission.""" + # Check if the user exists + users = self.db.getRecordset("users", recordFilter={"id": userId}) + if not users: + return False + + # Check if current user has permission + if not self._canModify("users", userId): + raise PermissionError(f"No permission to delete user {userId}") + + # Check if it's the initial user + initialUserId = self.getInitialId("users") + if initialUserId is not None and userId == initialUserId: + logger.warning("Attempt to delete the Root Admin was prevented") + return False + + # Delete all data associated with the user + self._deleteUserReferencedData(userId) + + # Delete the user + success = self.db.recordDelete("users", userId) + + if success: + logger.info(f"User with ID {userId} was successfully deleted") + else: + logger.error(f"Error deleting user with ID {userId}") + + return success + + +# Singleton factory for GatewayInterface instances per context +_gatewayInterfaces = {} + +def getGatewayInterface(mandateId: int = None, userId: int = None) -> GatewayInterface: + """ + Returns a GatewayInterface instance for the specified context. + Reuses existing instances. + """ + contextKey = f"{mandateId}_{userId}" + if contextKey not in _gatewayInterfaces: + _gatewayInterfaces[contextKey] = GatewayInterface(mandateId, userId) + return _gatewayInterfaces[contextKey] + +# Initialize an instance +getGatewayInterface() \ No newline at end of file diff --git a/static/38_email_preview.html b/static/38_email_preview.html new file mode 100644 index 00000000..1a3aba57 --- /dev/null +++ b/static/38_email_preview.html @@ -0,0 +1,49 @@ + + + +
+ +Hello Development Team,
+Please find attached the Python modules 'defAttributes.py' and 'gatewayInterface.py'. The 'defAttributes.py' file defines a Pydantic model for attribute definitions and includes a function to convert it into a list of AttributeDefinition objects. The 'gatewayInterface.py' file serves as an interface to the Gateway system, managing users and mandates.
Kindly review the attached files and provide your feedback.
+Best regards,
[Your Name]
Hello Development Team,
\nPlease find attached the Python modules 'defAttributes.py' and 'gatewayInterface.py'. The 'defAttributes.py' file defines a Pydantic model for attribute definitions and includes a function to convert it into a list of AttributeDefinition objects. The 'gatewayInterface.py' file serves as an interface to the Gateway system, managing users and mandates.
Kindly review the attached files and provide your feedback.
\nBest regards,
[Your Name]