Implemented role-based access control, Updated e2e authentication tests and Playwright setup.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,12 +8,17 @@ htmlcov
|
|||||||
.venv
|
.venv
|
||||||
tests/settings_from_unit_testing.json
|
tests/settings_from_unit_testing.json
|
||||||
tests/TestDBEngineRoot
|
tests/TestDBEngineRoot
|
||||||
|
tests/*.png
|
||||||
|
src/*.png
|
||||||
|
tests/*.html
|
||||||
test-results
|
test-results
|
||||||
.sesskey
|
.sesskey
|
||||||
tools.db
|
tools.db
|
||||||
.mytools_db
|
.mytools_db
|
||||||
.idea/MyManagingTools.iml
|
.idea/MyManagingTools.iml
|
||||||
.idea/misc.xml
|
.idea/misc.xml
|
||||||
|
.idea/dataSources.xml
|
||||||
|
.idea/sqldialects.xml
|
||||||
.idea_bak
|
.idea_bak
|
||||||
**/*.prof
|
**/*.prof
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class AuthManager:
|
|||||||
session["username"] = user_data["username"]
|
session["username"] = user_data["username"]
|
||||||
session["user_email"] = user_data["email"]
|
session["user_email"] = user_data["email"]
|
||||||
session["is_admin"] = bool(user_data["is_admin"])
|
session["is_admin"] = bool(user_data["is_admin"])
|
||||||
|
session["roles"] = UserDAO.get_user_roles_by_id(user_data["id"])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def logout_user(session) -> None:
|
def logout_user(session) -> None:
|
||||||
@@ -130,4 +131,3 @@ class AuthManager:
|
|||||||
"user_email": "admin@mmt.com",
|
"user_email": "admin@mmt.com",
|
||||||
"is_admin": True
|
"is_admin": True
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,9 +6,10 @@ class BaseComponent:
|
|||||||
Base class for all components that need to have a session and an id
|
Base class for all components that need to have a session and an id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session, _id=None, **kwargs):
|
def __init__(self, session, _id=None, role_name=None, **kwargs):
|
||||||
self._session = session
|
self._session = session
|
||||||
self._id = _id or self.create_component_id(session)
|
self._id = _id or self.create_component_id(session)
|
||||||
|
self._role_name = role_name
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
return self._id
|
return self._id
|
||||||
@@ -34,6 +35,17 @@ class BaseComponent:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def create_component_id(session):
|
def create_component_id(session):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __ft__(self):
|
||||||
|
if (not self._session["is_admin"] and
|
||||||
|
self._role_name is not None and
|
||||||
|
self._role_name not in self._session["roles"]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.render()
|
||||||
|
|
||||||
|
|
||||||
class BaseComponentSingleton(BaseComponent):
|
class BaseComponentSingleton(BaseComponent):
|
||||||
@@ -43,8 +55,8 @@ class BaseComponentSingleton(BaseComponent):
|
|||||||
|
|
||||||
COMPONENT_INSTANCE_ID = None
|
COMPONENT_INSTANCE_ID = None
|
||||||
|
|
||||||
def __init__(self, session, _id=None, settings_manager=None, tabs_manager=None, **kwargs):
|
def __init__(self, session, _id=None, role_name=None, settings_manager=None, tabs_manager=None, **kwargs):
|
||||||
super().__init__(session, _id, **kwargs)
|
super().__init__(session, _id, role_name,**kwargs)
|
||||||
self._settings_manager = settings_manager
|
self._settings_manager = settings_manager
|
||||||
self.tabs_manager = tabs_manager
|
self.tabs_manager = tabs_manager
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.addstuff.constants import ADD_STUFF_INSTANCE_ID
|
from components.addstuff.constants import ADD_STUFF_INSTANCE_ID, ADD_STUFF_ROLE
|
||||||
from components.repositories.components.Repositories import Repositories
|
from components.repositories.components.Repositories import Repositories
|
||||||
from core.instance_manager import InstanceManager
|
from core.instance_manager import InstanceManager
|
||||||
|
|
||||||
|
|
||||||
class AddStuffMenu(BaseComponent):
|
class AddStuffMenu(BaseComponent):
|
||||||
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id, ADD_STUFF_ROLE)
|
||||||
self.tabs_manager = tabs_manager # MyTabs component id
|
self.tabs_manager = tabs_manager # MyTabs component id
|
||||||
self.repositories = InstanceManager.get(session, Repositories.create_component_id(session), Repositories)
|
self.repositories = InstanceManager.get(session, Repositories.create_component_id(session), Repositories)
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div("Add stuff...", tabindex="0"),
|
Div("Add stuff...", tabindex="0"),
|
||||||
Ul(
|
Ul(
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
ADD_STUFF_INSTANCE_ID = "__AddStuff__"
|
ADD_STUFF_INSTANCE_ID = "__AddStuff__"
|
||||||
|
ADD_STUFF_ROLE = "add_stuff"
|
||||||
@@ -8,7 +8,7 @@ from components.admin.assets.icons import icon_jira
|
|||||||
from components.admin.commands import AdminCommandManager
|
from components.admin.commands import AdminCommandManager
|
||||||
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm, AdminButton, AdminMessageType
|
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm, AdminButton, AdminMessageType
|
||||||
from components.admin.components.ImportHolidays import ImportHolidays
|
from components.admin.components.ImportHolidays import ImportHolidays
|
||||||
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_JIRA_INSTANCE_ID
|
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_JIRA_INSTANCE_ID, ADMIN_ROLE
|
||||||
from components.aibuddy.assets.icons import icon_brain_ok
|
from components.aibuddy.assets.icons import icon_brain_ok
|
||||||
from components.hoildays.assets.icons import icon_holidays
|
from components.hoildays.assets.icons import icon_holidays
|
||||||
from components.tabs.components.MyTabs import MyTabs
|
from components.tabs.components.MyTabs import MyTabs
|
||||||
@@ -19,7 +19,7 @@ from core.jira import Jira
|
|||||||
|
|
||||||
class Admin(BaseComponent):
|
class Admin(BaseComponent):
|
||||||
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager: MyTabs = None):
|
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager: MyTabs = None):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id, ADMIN_ROLE)
|
||||||
self.settings_manager = settings_manager
|
self.settings_manager = settings_manager
|
||||||
self.tabs_manager = tabs_manager
|
self.tabs_manager = tabs_manager
|
||||||
self.commands = AdminCommandManager(self)
|
self.commands = AdminCommandManager(self)
|
||||||
@@ -117,7 +117,7 @@ class Admin(BaseComponent):
|
|||||||
form.set_message(f"Error {res.status_code} - {res.text}", AdminMessageType.ERROR)
|
form.set_message(f"Error {res.status_code} - {res.text}", AdminMessageType.ERROR)
|
||||||
return self.tabs_manager.render()
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(cls="divider"),
|
Div(cls="divider"),
|
||||||
Div(mk_ellipsis("Admin", cls="text-sm font-medium mb-1 mr-3")),
|
Div(mk_ellipsis("Admin", cls="text-sm font-medium mb-1 mr-3")),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
ADMIN_INSTANCE_ID = "__Admin__"
|
ADMIN_INSTANCE_ID = "__Admin__"
|
||||||
|
ADMIN_ROLE = "admin"
|
||||||
ADMIN_AI_BUDDY_INSTANCE_ID = "__AdminAIBuddy__"
|
ADMIN_AI_BUDDY_INSTANCE_ID = "__AdminAIBuddy__"
|
||||||
ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID = "__AdminImportHolidays__"
|
ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID = "__AdminImportHolidays__"
|
||||||
ADMIN_JIRA_INSTANCE_ID = "__AdminJira__"
|
ADMIN_JIRA_INSTANCE_ID = "__AdminJira__"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from core.settings_management import GenericDbManager
|
|||||||
|
|
||||||
class AIBuddy(BaseComponent):
|
class AIBuddy(BaseComponent):
|
||||||
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager=None):
|
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id, AI_BUDDY_ROLE)
|
||||||
self.settings_manager = settings_manager
|
self.settings_manager = settings_manager
|
||||||
self.db = GenericDbManager(session, settings_manager, AI_BUDDY_SETTINGS_ENTRY, AIBuddySettings)
|
self.db = GenericDbManager(session, settings_manager, AI_BUDDY_SETTINGS_ENTRY, AIBuddySettings)
|
||||||
self.tabs_manager = tabs_manager
|
self.tabs_manager = tabs_manager
|
||||||
@@ -153,7 +153,7 @@ class AIBuddy(BaseComponent):
|
|||||||
for name, tool in available_tools.items()
|
for name, tool in available_tools.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(cls="divider"),
|
Div(cls="divider"),
|
||||||
Div(
|
Div(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
AI_BUDDY_INSTANCE_ID = "__AIBuddy__"
|
AI_BUDDY_INSTANCE_ID = "__AIBuddy__"
|
||||||
|
AI_BUDDY_ROLE = "ai_buddy"
|
||||||
ROUTE_ROOT = "/ai"
|
ROUTE_ROOT = "/ai"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from fasthtml.components import *
|
|||||||
|
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.applications.commands import Commands
|
from components.applications.commands import Commands
|
||||||
from components.applications.constants import APPLICATION_INSTANCE_ID
|
from components.applications.constants import APPLICATION_INSTANCE_ID, APPLICATION_ROLE
|
||||||
from components.hoildays.assets.icons import icon_holidays
|
from components.hoildays.assets.icons import icon_holidays
|
||||||
from components.hoildays.components.HolidaysViewer import HolidaysViewer
|
from components.hoildays.components.HolidaysViewer import HolidaysViewer
|
||||||
from components.hoildays.constants import HOLIDAYS_VIEWER_INSTANCE_ID
|
from components.hoildays.constants import HOLIDAYS_VIEWER_INSTANCE_ID
|
||||||
@@ -12,7 +12,7 @@ from core.instance_manager import InstanceManager
|
|||||||
|
|
||||||
class Applications(BaseComponent):
|
class Applications(BaseComponent):
|
||||||
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id, APPLICATION_ROLE)
|
||||||
self.tabs_manager = tabs_manager
|
self.tabs_manager = tabs_manager
|
||||||
self.settings_manager = settings_manager
|
self.settings_manager = settings_manager
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
@@ -30,7 +30,7 @@ class Applications(BaseComponent):
|
|||||||
|
|
||||||
raise NotImplementedError(app_name)
|
raise NotImplementedError(app_name)
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(cls="divider"),
|
Div(cls="divider"),
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
APPLICATION_INSTANCE_ID = "__Applications__"
|
APPLICATION_INSTANCE_ID = "__Applications__"
|
||||||
|
APPLICATION_ROLE = "applications"
|
||||||
ROUTE_ROOT = "/apps"
|
ROUTE_ROOT = "/apps"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from components.aibuddy.components.AIBuddy import AIBuddy
|
|||||||
from components.debugger.assets.icons import icon_dbengine
|
from components.debugger.assets.icons import icon_dbengine
|
||||||
from components.debugger.commands import Commands
|
from components.debugger.commands import Commands
|
||||||
from components.debugger.components.JsonViewer import JsonViewer
|
from components.debugger.components.JsonViewer import JsonViewer
|
||||||
from components.debugger.constants import DBENGINE_DEBUGGER_INSTANCE_ID
|
from components.debugger.constants import DBENGINE_DEBUGGER_INSTANCE_ID, DEBUGGER_ROLE
|
||||||
from components_helpers import mk_ellipsis, mk_accordion_section
|
from components_helpers import mk_ellipsis, mk_accordion_section
|
||||||
from core.instance_manager import InstanceManager
|
from core.instance_manager import InstanceManager
|
||||||
from core.utils import get_unique_id
|
from core.utils import get_unique_id
|
||||||
@@ -20,7 +20,7 @@ logger = logging.getLogger("Debugger")
|
|||||||
|
|
||||||
class Debugger(BaseComponent):
|
class Debugger(BaseComponent):
|
||||||
def __init__(self, session, _id, settings_manager, tabs_manager):
|
def __init__(self, session, _id, settings_manager, tabs_manager):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id, DEBUGGER_ROLE)
|
||||||
self.settings_manager = settings_manager
|
self.settings_manager = settings_manager
|
||||||
self.db_engine = settings_manager.get_db_engine()
|
self.db_engine = settings_manager.get_db_engine()
|
||||||
self.tabs_manager = tabs_manager
|
self.tabs_manager = tabs_manager
|
||||||
@@ -104,7 +104,7 @@ class Debugger(BaseComponent):
|
|||||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||||
return self.tabs_manager.render()
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(cls="divider"),
|
Div(cls="divider"),
|
||||||
mk_ellipsis("Debugger", cls="text-sm font-medium mb-1"),
|
mk_ellipsis("Debugger", cls="text-sm font-medium mb-1"),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
DBENGINE_DEBUGGER_INSTANCE_ID = "debugger"
|
DBENGINE_DEBUGGER_INSTANCE_ID = "debugger"
|
||||||
|
DEBUGGER_ROLE = "debugger"
|
||||||
ROUTE_ROOT = "/debugger"
|
ROUTE_ROOT = "/debugger"
|
||||||
|
|
||||||
INDENT_SIZE = 20
|
INDENT_SIZE = 20
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from components.datagrid_new.components.DataGrid import DataGrid
|
|||||||
from components.form.components.MyForm import MyForm, FormField
|
from components.form.components.MyForm import MyForm, FormField
|
||||||
from components.repositories.assets.icons import icon_database, icon_table
|
from components.repositories.assets.icons import icon_database, icon_table
|
||||||
from components.repositories.commands import Commands
|
from components.repositories.commands import Commands
|
||||||
from components.repositories.constants import REPOSITORIES_INSTANCE_ID, ROUTE_ROOT, Routes
|
from components.repositories.constants import REPOSITORIES_INSTANCE_ID, ROUTE_ROOT, Routes, REPOSITORIES_ROLE
|
||||||
from components.repositories.db_management import RepositoriesDbManager, Repository
|
from components.repositories.db_management import RepositoriesDbManager, Repository
|
||||||
from components_helpers import mk_icon, mk_ellipsis
|
from components_helpers import mk_icon, mk_ellipsis
|
||||||
from core.instance_manager import InstanceManager
|
from core.instance_manager import InstanceManager
|
||||||
@@ -19,7 +19,7 @@ logger = logging.getLogger("Repositories")
|
|||||||
|
|
||||||
class Repositories(BaseComponent):
|
class Repositories(BaseComponent):
|
||||||
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id)
|
super().__init__(session, _id, REPOSITORIES_ROLE)
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
self.tabs_manager = tabs_manager
|
self.tabs_manager = tabs_manager
|
||||||
self.db = RepositoriesDbManager(session, settings_manager)
|
self.db = RepositoriesDbManager(session, settings_manager)
|
||||||
@@ -121,7 +121,7 @@ class Repositories(BaseComponent):
|
|||||||
def refresh(self):
|
def refresh(self):
|
||||||
return self._mk_repositories(oob=True)
|
return self._mk_repositories(oob=True)
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(cls="divider"),
|
Div(cls="divider"),
|
||||||
mk_ellipsis("Repositories", cls="text-sm font-medium mb-1"),
|
mk_ellipsis("Repositories", cls="text-sm font-medium mb-1"),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
REPOSITORIES_INSTANCE_ID = "__Repositories__"
|
REPOSITORIES_INSTANCE_ID = "__Repositories__"
|
||||||
|
REPOSITORIES_ROLE = "repositories"
|
||||||
ROUTE_ROOT = "/repositories"
|
ROUTE_ROOT = "/repositories"
|
||||||
USERS_REPOSITORY_NAME = "__USERS___"
|
USERS_REPOSITORY_NAME = "__USERS___"
|
||||||
HOLIDAYS_TABLE_NAME = "__HOLIDAYS__"
|
HOLIDAYS_TABLE_NAME = "__HOLIDAYS__"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class UndoRedo(BaseComponentSingleton):
|
|||||||
COMPONENT_INSTANCE_ID = UNDO_REDO_INSTANCE_ID
|
COMPONENT_INSTANCE_ID = UNDO_REDO_INSTANCE_ID
|
||||||
|
|
||||||
def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
|
def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id, settings_manager, tabs_manager)
|
super().__init__(session, _id, None, settings_manager, tabs_manager)
|
||||||
self.index = -1
|
self.index = -1
|
||||||
self.history = []
|
self.history = []
|
||||||
self._commands = UndoRedoCommandManager(self)
|
self._commands = UndoRedoCommandManager(self)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from components.BaseComponent import BaseComponentSingleton
|
|||||||
from components.form.components.MyForm import MyForm, FormField
|
from components.form.components.MyForm import MyForm, FormField
|
||||||
from components.workflows.commands import WorkflowsCommandManager
|
from components.workflows.commands import WorkflowsCommandManager
|
||||||
from components.workflows.components.WorkflowDesigner import WorkflowDesigner
|
from components.workflows.components.WorkflowDesigner import WorkflowDesigner
|
||||||
from components.workflows.constants import WORKFLOWS_INSTANCE_ID
|
from components.workflows.constants import WORKFLOWS_INSTANCE_ID, WORKFLOWS_ROLE
|
||||||
from components.workflows.db_management import WorkflowsDbManager, WorkflowsDesignerSettings
|
from components.workflows.db_management import WorkflowsDbManager, WorkflowsDesignerSettings
|
||||||
from components_helpers import mk_ellipsis, mk_icon
|
from components_helpers import mk_ellipsis, mk_icon
|
||||||
from core.instance_manager import InstanceManager
|
from core.instance_manager import InstanceManager
|
||||||
@@ -19,7 +19,7 @@ class Workflows(BaseComponentSingleton):
|
|||||||
COMPONENT_INSTANCE_ID = WORKFLOWS_INSTANCE_ID
|
COMPONENT_INSTANCE_ID = WORKFLOWS_INSTANCE_ID
|
||||||
|
|
||||||
def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
|
def __init__(self, session, _id, settings_manager=None, tabs_manager=None):
|
||||||
super().__init__(session, _id, settings_manager, tabs_manager)
|
super().__init__(session, _id, WORKFLOWS_ROLE, settings_manager, tabs_manager)
|
||||||
self.commands = WorkflowsCommandManager(self)
|
self.commands = WorkflowsCommandManager(self)
|
||||||
self.db = WorkflowsDbManager(session, settings_manager)
|
self.db = WorkflowsDbManager(session, settings_manager)
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class Workflows(BaseComponentSingleton):
|
|||||||
def refresh(self):
|
def refresh(self):
|
||||||
return self._mk_workflows(True)
|
return self._mk_workflows(True)
|
||||||
|
|
||||||
def __ft__(self):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(cls="divider"),
|
Div(cls="divider"),
|
||||||
Div(
|
Div(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
WORKFLOWS_INSTANCE_ID = "__Workflows__"
|
WORKFLOWS_INSTANCE_ID = "__Workflows__"
|
||||||
|
WORKFLOWS_ROLE = "workflows"
|
||||||
WORKFLOW_DESIGNER_INSTANCE_ID = "__WorkflowDesigner__"
|
WORKFLOW_DESIGNER_INSTANCE_ID = "__WorkflowDesigner__"
|
||||||
WORKFLOW_PLAYER_INSTANCE_ID = "__WorkflowPlayer__"
|
WORKFLOW_PLAYER_INSTANCE_ID = "__WorkflowPlayer__"
|
||||||
WORKFLOWS_DB_ENTRY = "Workflows"
|
WORKFLOWS_DB_ENTRY = "Workflows"
|
||||||
|
|||||||
@@ -56,22 +56,35 @@ class UserDAO:
|
|||||||
VALUES (?, ?, ?, ?, ?, 0)
|
VALUES (?, ?, ?, ?, ?, 0)
|
||||||
''', (username, email, password_hash, salt, github_id))
|
''', (username, email, password_hash, salt, github_id))
|
||||||
|
|
||||||
|
# Get the ID of the newly created user
|
||||||
|
user_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# Add default roles to the new user
|
||||||
|
default_roles = ['repositories', 'datagrid', 'ai_buddy']
|
||||||
|
for role in default_roles:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO roles (user_id, role_name)
|
||||||
|
VALUES (?, ?)
|
||||||
|
''', (user_id, role))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.lastrowid
|
return user_id
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating user: {e}")
|
logger.error(f"Error creating user: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def authenticate_email(email: str, password: str, db_instance=None) -> dict[str, Any] | None:
|
def authenticate_email(email: str, password: str, db_instance=None) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Authenticate a user with email and password.
|
Authenticate a user with email and password.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
email: The user's email
|
email: The user's email
|
||||||
password: The user's password
|
password: The user's password
|
||||||
db_instance: Database instance (optional, uses default if None)
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict or None: User record if authentication succeeds, None otherwise
|
Dict or None: User record if authentication succeeds, None otherwise
|
||||||
"""
|
"""
|
||||||
@@ -106,18 +119,19 @@ class UserDAO:
|
|||||||
# Return user info
|
# Return user info
|
||||||
return dict(user)
|
return dict(user)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_or_create_github_user(github_id: str, username: str, email: str | None, db_instance=None) -> dict[
|
def find_or_create_github_user(github_id: str, username: str, email: str | None, db_instance=None) -> dict[
|
||||||
str, Any] | None:
|
str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Find existing GitHub user or create a new one.
|
Find existing GitHub user or create a new one.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
github_id: GitHub user ID
|
github_id: GitHub user ID
|
||||||
username: The username from GitHub
|
username: The username from GitHub
|
||||||
email: The email from GitHub (may be None)
|
email: The email from GitHub (may be None)
|
||||||
db_instance: Database instance (optional, uses default if None)
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict or None: User record if found or created, None on error
|
Dict or None: User record if found or created, None on error
|
||||||
"""
|
"""
|
||||||
@@ -165,15 +179,16 @@ class UserDAO:
|
|||||||
logger.error(f"Error creating GitHub user: {e}")
|
logger.error(f"Error creating GitHub user: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_by_id(user_id: int, db_instance=None) -> dict[str, Any] | None:
|
def get_user_by_id(user_id: int, db_instance=None) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Get a user by ID.
|
Get a user by ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The user ID
|
user_id: The user ID
|
||||||
db_instance: Database instance (optional, uses default if None)
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict or None: User record if found, None otherwise
|
Dict or None: User record if found, None otherwise
|
||||||
"""
|
"""
|
||||||
@@ -189,16 +204,44 @@ class UserDAO:
|
|||||||
user = cursor.fetchone()
|
user = cursor.fetchone()
|
||||||
return dict(user) if user else None
|
return dict(user) if user else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_roles_by_id(user_id: int, db_instance=None) -> list[str]:
|
||||||
|
"""
|
||||||
|
Retrieve the roles associated with a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (int): User ID.
|
||||||
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: List of role names associated with the user.
|
||||||
|
"""
|
||||||
|
db_instance = db_instance or get_user_db()
|
||||||
|
|
||||||
|
with db_instance.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT role_name
|
||||||
|
FROM roles
|
||||||
|
WHERE user_id = ?
|
||||||
|
''', (user_id,))
|
||||||
|
|
||||||
|
# get the roles
|
||||||
|
roles = [row["role_name"] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
return roles
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_users(limit: int = 100, offset: int = 0, db_instance=None) -> list[dict[str, Any]]:
|
def get_all_users(limit: int = 100, offset: int = 0, db_instance=None) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Get all users with pagination.
|
Get all users with pagination.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
limit: Maximum number of users to return
|
limit: Maximum number of users to return
|
||||||
offset: Number of users to skip
|
offset: Number of users to skip
|
||||||
db_instance: Database instance (optional, uses default if None)
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of user records
|
List of user records
|
||||||
"""
|
"""
|
||||||
@@ -215,22 +258,23 @@ class UserDAO:
|
|||||||
last_login,
|
last_login,
|
||||||
(github_id IS NOT NULL) as is_github_user
|
(github_id IS NOT NULL) as is_github_user
|
||||||
FROM users
|
FROM users
|
||||||
ORDER BY created_at DESC LIMIT ?
|
ORDER BY created_at DESC
|
||||||
OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
''', (limit, offset))
|
''', (limit, offset))
|
||||||
|
|
||||||
return [dict(user) for user in cursor.fetchall()]
|
return [dict(user) for user in cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_admin_status(user_id: int, is_admin: bool, db_instance=None) -> bool:
|
def set_admin_status(user_id: int, is_admin: bool, db_instance=None) -> bool:
|
||||||
"""
|
"""
|
||||||
Change a user's admin status.
|
Change a user's admin status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The user ID
|
user_id: The user ID
|
||||||
is_admin: True to make admin, False to remove admin status
|
is_admin: True to make admin, False to remove admin status
|
||||||
db_instance: Database instance (optional, uses default if None)
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful, False otherwise
|
bool: True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
@@ -250,15 +294,16 @@ class UserDAO:
|
|||||||
logger.error(f"Error setting admin status: {e}")
|
logger.error(f"Error setting admin status: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_user(user_id: int, db_instance=None) -> bool:
|
def delete_user(user_id: int, db_instance=None) -> bool:
|
||||||
"""
|
"""
|
||||||
Delete a user and all their data.
|
Delete a user and all their data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The user ID
|
user_id: The user ID
|
||||||
db_instance: Database instance (optional, uses default if None)
|
db_instance: Database instance (optional, uses default if None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful, False otherwise
|
bool: True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ class Database:
|
|||||||
''')
|
''')
|
||||||
logger.info("Created users table")
|
logger.info("Created users table")
|
||||||
|
|
||||||
|
# Create the roles table
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER,
|
||||||
|
role_name TEXT NOT NULL,
|
||||||
|
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
logger.info("Created roles table")
|
||||||
|
|
||||||
# Check if we need to create an admin user
|
# Check if we need to create an admin user
|
||||||
cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = 1")
|
cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = 1")
|
||||||
|
|||||||
187
tests/e2e/Readme.md
Normal file
187
tests/e2e/Readme.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# README - End-to-End Authentication Testing Guide
|
||||||
|
|
||||||
|
This README provides details on how to set up, configure, and run the end-to-end (e2e) authentication tests for the **My Managing Tools** application. The tests are implemented using `pytest` with `playwright` for browser automation.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The e2e tests verify that the authentication system works as expected, including login functionality, session validation, and database isolation for testing purposes. These tests ensure the login page behaves properly under different scenarios such as unauthenticated access, form validation, and successful user login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Setup Instructions](#setup-instructions)
|
||||||
|
- [Application Setup](#application-setup)
|
||||||
|
- [Database Setup](#database-setup)
|
||||||
|
2. [Installing Dependencies](#installing-dependencies)
|
||||||
|
3. [Running Tests](#running-tests)
|
||||||
|
4. [Debugging and Logs](#debugging-and-logs)
|
||||||
|
5. [Test Scenarios](#test-scenarios)
|
||||||
|
6. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
Before running the tests, make sure the environment is set up correctly for both the application and the test database.
|
||||||
|
|
||||||
|
### Application Setup
|
||||||
|
|
||||||
|
1. **Install Required Dependencies** (Python version 3.9 or higher is recommended):
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the Application**:
|
||||||
|
Navigate to the `src` directory and start the application:
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
Alternatively, use Docker:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server starts, ensure the app is accessible at the base URL defined in `playwright_config.py`. By default, it should be `http://localhost:5002`.
|
||||||
|
|
||||||
|
### Database Setup
|
||||||
|
|
||||||
|
The e2e tests are designed to interact with an **isolated test database**. During setup, a temporary SQLite database will be created for each test session.
|
||||||
|
|
||||||
|
1. **Test Database Initialization**:
|
||||||
|
The database is initialized dynamically during the test execution using a custom temporary path. The `test_database` fixture creates a database with the following characteristics:
|
||||||
|
- **Temporary Directory**: Ensures no conflicts with the production database.
|
||||||
|
- **Admin User Auto-Creation**: Adds an admin account if the environment variables `ADMIN_EMAIL` and `ADMIN_PASSWORD` are set.
|
||||||
|
|
||||||
|
2. **Environment Variables**:
|
||||||
|
Ensure the following environment variables are defined for the test database:
|
||||||
|
```env
|
||||||
|
DB_PATH="test_mmt_tools.db"
|
||||||
|
ADMIN_EMAIL="test.admin@test.com"
|
||||||
|
ADMIN_PASSWORD="TestAdmin123"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configuration**:
|
||||||
|
The `test_database.py` script handles the database isolation and cleanup after each test session. It ensures that no state is leaked between tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installing Dependencies
|
||||||
|
|
||||||
|
Install the required packages for the tests:
|
||||||
|
|
||||||
|
1. **Playwright and Pytest**:
|
||||||
|
Install `pytest-playwright` and playwright dependencies:
|
||||||
|
```bash
|
||||||
|
pip install pytest-playwright
|
||||||
|
playwright install
|
||||||
|
playwright install-deps # For Linux systems
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Project Dependencies**:
|
||||||
|
Use the provided `requirements.txt` to install other necessary packages:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify Installation**:
|
||||||
|
Confirm Playwright is installed and functional:
|
||||||
|
```bash
|
||||||
|
playwright --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
To execute the end-to-end authentication tests, run the following commands from the project root.
|
||||||
|
|
||||||
|
1. **Run All Tests**:
|
||||||
|
Execute all e2e tests:
|
||||||
|
```bash
|
||||||
|
pytest -m "e2e"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run Specific Test**:
|
||||||
|
Use the test path and `-k` argument to target specific tests:
|
||||||
|
```bash
|
||||||
|
pytest tests/test_e2e_authentication.py -k "test_unauthenticated_user_redirected_to_login"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Record Test Results**:
|
||||||
|
Save test results (HTML report):
|
||||||
|
```bash
|
||||||
|
pytest --html=test-results/report.html --self-contained-html
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Parallel Execution**:
|
||||||
|
If desired, run tests in parallel:
|
||||||
|
```bash
|
||||||
|
pytest -n auto
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging and Logs
|
||||||
|
|
||||||
|
1. **Console Logs**:
|
||||||
|
Console errors during page navigation are captured using the `test_page_loads_without_errors` test. Check the pytest output for details.
|
||||||
|
|
||||||
|
2. **Debugging Tools**:
|
||||||
|
- Videos and HAR traces are recorded in the `test-results` directory.
|
||||||
|
- Use the `page.pause()` method inside a test for interactive debugging with Playwright Inspector.
|
||||||
|
|
||||||
|
3. **Screenshots and Debug HTML**:
|
||||||
|
If a test fails, screenshots and full HTML are saved for post-mortem debugging:
|
||||||
|
- `debug_after_login.png`: Screenshot post-login.
|
||||||
|
- `debug_page.html`: Captured HTML structure of the page.
|
||||||
|
|
||||||
|
4. **Server Logs**:
|
||||||
|
When using the `app_server` fixture, server `stdout` and `stderr` are recorded during the test execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
The main authentication tests cover the following scenarios:
|
||||||
|
|
||||||
|
1. **Unauthenticated User Redirect**:
|
||||||
|
Verify that unauthenticated users accessing protected resources are redirected to the login page.
|
||||||
|
|
||||||
|
2. **Login Page Elements**:
|
||||||
|
Validate that key login page elements are visible, such as email/password fields and the submit button.
|
||||||
|
|
||||||
|
3. **Successful Login**:
|
||||||
|
Ensure users can log in successfully with valid credentials and are redirected to the home page.
|
||||||
|
|
||||||
|
4. **Test Database Isolation**:
|
||||||
|
Confirm that the test environment uses an isolated temporary database and does not affect production data.
|
||||||
|
|
||||||
|
5. **Page Load Without Errors**:
|
||||||
|
Ensure that the login page loads without console or HTTP errors.
|
||||||
|
|
||||||
|
For a full list of test cases, see `tests/test_e2e_authentication.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Server Not Starting**:
|
||||||
|
If the server fails to start during testing, confirm the `DB_PATH`, `BASE_URL`, and all dependencies are correctly configured in the `.env` file.
|
||||||
|
|
||||||
|
- **Playwright Issues**:
|
||||||
|
Ensure Playwright dependencies (`playwright install`) are installed and functional. Missing browsers or outdated Playwright versions can cause tests to fail.
|
||||||
|
|
||||||
|
- **Database Error**:
|
||||||
|
Check that the `tools.db` file or the temporary test database exists and is accessible. Ensure the test database environment variables are set correctly during tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
- The app server is started with test-specific configurations (`app_server` fixture) that ensure end-to-end isolation.
|
||||||
|
- Tests requiring authentication use an admin user defined in the `test_users` fixture:
|
||||||
|
- Email: `test.admin@test.com`
|
||||||
|
- Password: `TestAdmin123`
|
||||||
|
- Ensure no sensitive data or configurations are hardcoded in the test scripts.
|
||||||
0
tests/e2e/__init__.py
Normal file
0
tests/e2e/__init__.py
Normal file
Reference in New Issue
Block a user