diff --git a/common/services/tenant_service.py b/common/services/tenant_service.py
new file mode 100644
index 0000000..a925b60
--- /dev/null
+++ b/common/services/tenant_service.py
@@ -0,0 +1,71 @@
+from flask import session, current_app
+from sqlalchemy.exc import SQLAlchemyError
+
+from common.extensions import db
+from common.models.user import Partner, PartnerTenant
+from common.utils.eveai_exceptions import EveAINoManagementPartnerService
+from common.utils.model_logging_utils import set_logging_information
+from datetime import datetime as dt, timezone as tz
+
+from common.utils.security_utils import current_user_has_role
+
+
+class TenantService:
+ @staticmethod
+ def associate_tenant_with_partner(tenant_id):
+ """Associate a tenant with a partner"""
+ try:
+ partner_id = session['partner']['id']
+ # Get partner service (MANAGEMENT_SERVICE type)
+ partner = Partner.query.get(partner_id)
+ if not partner:
+ return
+
+ # Find a management service for this partner
+ management_service = next((service for service in session['partner']['services']
+ if service.get('type') == 'MANAGEMENT_SERVICE'), None)
+
+ if not management_service:
+ current_app.logger.error(f"No Management Service defined for partner {partner_id}"
+ f"while associating tenant {tenant_id} with partner.")
+ raise EveAINoManagementPartnerService()
+
+ # Create the association
+ tenant_partner = PartnerTenant(
+ partner_service_id=management_service['id'],
+ tenant_id=tenant_id,
+ relationship_type='MANAGED',
+ )
+ set_logging_information(tenant_partner, dt.now(tz.utc))
+
+ db.session.add(tenant_partner)
+ db.session.commit()
+
+ except SQLAlchemyError as e:
+ db.session.rollback()
+ current_app.logger.error(f"Error associating tenant {tenant_id} with partner: {str(e)}")
+ raise e
+
+ @staticmethod
+ def can_user_edit_tenant(tenant_id) -> bool:
+ if current_user_has_role('Super User'):
+ return True
+ elif current_user_has_role('Partner Admin'):
+ partner_id = session['partner']['id']
+ partner_service = next((service for service in session['partner']['services']
+ if service.get('type') == 'MANAGEMENT_SERVICE'), None)
+ if not partner_service:
+ return False
+ else:
+ partner_tenant = PartnerTenant.query.filter(
+ PartnerTenant.tenant_id == tenant_id,
+ PartnerTenant.partner_service_id == partner_service['id'],
+ ).first()
+ if partner_tenant:
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
diff --git a/common/services/user_service.py b/common/services/user_service.py
index beba4c3..be666ff 100644
--- a/common/services/user_service.py
+++ b/common/services/user_service.py
@@ -14,26 +14,21 @@ class UserService:
and the active tenant for the session"""
current_tenant_id = session.get('tenant').get('id', None)
effective_role_names = []
- if current_tenant_id:
+ if current_tenant_id == 1:
if current_user_has_role("Super User"):
- if current_tenant_id == 1:
- effective_role_names.append("Super User")
- if session.get('partner'):
- effective_role_names.append("Partner Admin")
- effective_role_names.append("Tenant Admin")
+ effective_role_names.append("Super User")
+ elif current_tenant_id:
if current_user_has_role("Tenant Admin"):
effective_role_names.append("Tenant Admin")
- if current_user_has_role("Partner Admin"):
+ if current_user_has_role("Partner Admin") or current_user_has_role("Super User"):
effective_role_names.append("Tenant Admin")
if session.get('partner'):
if session.get('partner').get('tenant_id') == current_tenant_id:
effective_role_names.append("Partner Admin")
- effective_role_names = list(set(effective_role_names))
- effective_roles = [(role.id, role.name) for role in
- Role.query.filter(Role.name.in_(effective_role_names)).all()]
- return effective_roles
- else:
- return []
+ effective_role_names = list(set(effective_role_names))
+ effective_roles = [(role.id, role.name) for role in
+ Role.query.filter(Role.name.in_(effective_role_names)).all()]
+ return effective_roles
@staticmethod
def validate_role_assignments(role_ids):
diff --git a/common/utils/eveai_exceptions.py b/common/utils/eveai_exceptions.py
index 77eab11..9622661 100644
--- a/common/utils/eveai_exceptions.py
+++ b/common/utils/eveai_exceptions.py
@@ -154,3 +154,35 @@ class EveAIRoleAssignmentException(EveAIException):
def __init__(self, message, status_code=403, payload=None):
super().__init__(message, status_code, payload)
+
+class EveAINoManagementPartnerService(EveAIException):
+ """Exception raised when the operation requires the logged in partner (or selected parter by Super User)
+ does not have a MANAGEMENT_SERVICE"""
+
+ def __init__(self, message="No Management Service defined for partner", status_code=403, payload=None):
+ super().__init__(message, status_code, payload)
+
+
+class EveAINoSessionTenant(EveAIException):
+ """Exception raised when no session tenant is set"""
+
+ def __init__(self, message="No Session Tenant selected. Cannot perform requested action.", status_code=403,
+ payload=None):
+ super().__init__(message, status_code, payload)
+
+
+class EveAINoSessionPartner(EveAIException):
+ """Exception raised when no session partner is set"""
+
+ def __init__(self, message="No Session Partner selected. Cannot perform requested action.", status_code=403,
+ payload=None):
+ super().__init__(message, status_code, payload)
+
+
+class EveAINoManagementPartnerForTenant(EveAIException):
+ """Exception raised when the selected partner is no management partner for tenant"""
+
+ def __init__(self, message="No Management Partner for Tenant", status_code=403, payload=None):
+ super().__init__(message, status_code, payload)
+
+
diff --git a/common/utils/middleware.py b/common/utils/middleware.py
index 4722817..5e2b67b 100644
--- a/common/utils/middleware.py
+++ b/common/utils/middleware.py
@@ -5,9 +5,10 @@ for handling tenant requests
from flask_security import current_user
from flask import session, current_app, redirect
-from common.utils.nginx_utils import prefixed_url_for
-
from .database import Database
+from .eveai_exceptions import EveAINoSessionTenant, EveAINoSessionPartner, EveAINoManagementPartnerService, \
+ EveAINoManagementPartnerForTenant
+from ..services.tenant_service import TenantService
def mw_before_request():
@@ -17,17 +18,27 @@ def mw_before_request():
"""
if 'tenant' not in session:
- current_app.logger.warning('No tenant defined in session')
- return redirect(prefixed_url_for('security_bp.login'))
+ raise EveAINoSessionTenant()
tenant_id = session['tenant']['id']
if not tenant_id:
- raise Exception('Cannot switch schema for tenant: no tenant defined in session')
+ raise EveAINoSessionTenant()
- # user = User.query.get(current_user.id)
- if current_user.has_role('Super User') or current_user.tenant_id == tenant_id:
- Database(tenant_id).switch_schema()
- else:
- raise Exception(f'Cannot switch schema for tenant {tenant_id}: user {current_user.email} does not have access')
+ switch_allowed = False
+ if current_user.has_role('Super User'):
+ switch_allowed = True
+ if current_user.has_role('Tenant Admin') and current_user.tenant_id == tenant_id:
+ switch_allowed = True
+ if current_user.has_role('Partner Admin'):
+ if 'partner' not in session:
+ raise EveAINoSessionPartner()
+ management_service = next((service for service in session['partner']['services']
+ if service.get('type') == 'MANAGEMENT_SERVICE'), None)
+ if not management_service:
+ raise EveAINoManagementPartnerService()
+ if not TenantService.can_user_edit_tenant(tenant_id):
+ raise EveAINoManagementPartnerForTenant()
+
+ Database(tenant_id).switch_schema()
diff --git a/eveai_app/templates/navbar.html b/eveai_app/templates/navbar.html
index 7b2ef81..0f8d45f 100644
--- a/eveai_app/templates/navbar.html
+++ b/eveai_app/templates/navbar.html
@@ -69,38 +69,38 @@
{% if current_user.is_authenticated %}
{{ dropdown('Tenant Configuration', 'source_environment', [
- {'name': 'Tenants', 'url': '/user/select_tenant', 'roles': ['Super User']},
- {'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Tenant Admin']},
+ {'name': 'Tenants', 'url': '/user/select_tenant', 'roles': ['Super User', 'Partner Admin']},
+ {'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }}
{% endif %}
{% if current_user.is_authenticated %}
{{ dropdown('Document Mgmt', 'note_stack', [
- {'name': 'Catalogs', 'url': '/document/catalogs', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Processors', 'url': '/document/processors', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Retrievers', 'url': '/document/retrievers', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Documents', 'url': '/document/documents', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Document Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Tenant Admin']},
+ {'name': 'Catalogs', 'url': '/document/catalogs', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Processors', 'url': '/document/processors', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Retrievers', 'url': '/document/retrievers', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Add Document', 'url': '/document/add_document', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Add URL', 'url': '/document/add_url', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Documents', 'url': '/document/documents', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Document Versions', 'url': '/document/document_versions_list', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Library Operations', 'url': '/document/library_operations', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }}
{% endif %}
{% if current_user.is_authenticated %}
{{ dropdown('Interactions', 'hub', [
- {'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Tenant Admin']},
+ {'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
+ {'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
]) }}
{% endif %}
{% if current_user.is_authenticated %}
{{ dropdown('Administration', 'settings', [
- {'name': 'License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User']},
+ {'name': 'License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User', 'Partner Admin']},
{'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']},
- {'name': 'Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']},
+ {'name': 'Licenses', 'url': '/entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']},
+ {'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin', 'Partner Admin']},
{'name': 'Partners', 'url': '/administration/partners', 'roles': ['Super User']},
{'name': 'Partner Services', 'url': '/administration/partner_services', 'roles': ['Super User']},
]) }}
@@ -125,7 +125,7 @@
{% endif %}
- {% if current_user.has_roles('Super User') and 'partner' in session %}
+ {% if 'partner' in session %}
-
PARTNER {{ session['partner'].get('id', 'None') }}: {{ session['partner'].get('name', 'None') }}
diff --git a/eveai_app/views/basic_views.py b/eveai_app/views/basic_views.py
index 9add869..b5e0fed 100644
--- a/eveai_app/views/basic_views.py
+++ b/eveai_app/views/basic_views.py
@@ -37,7 +37,7 @@ def confirm_email_fail():
@basic_bp.route('/session_defaults', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def session_defaults():
try:
# Get tenant session
diff --git a/eveai_app/views/document_views.py b/eveai_app/views/document_views.py
index f1e89ef..c856c9d 100644
--- a/eveai_app/views/document_views.py
+++ b/eveai_app/views/document_views.py
@@ -53,7 +53,7 @@ def before_request():
@document_bp.route('/catalog', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def catalog():
form = CatalogForm()
@@ -80,7 +80,7 @@ def catalog():
@document_bp.route('/catalogs', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def catalogs():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -98,7 +98,7 @@ def catalogs():
@document_bp.route('/handle_catalog_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_catalog_selection():
action = request.form['action']
if action == 'create_catalog':
@@ -119,7 +119,7 @@ def handle_catalog_selection():
@document_bp.route('/catalog/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_catalog(catalog_id):
catalog = Catalog.query.get_or_404(catalog_id)
tenant_id = session.get('tenant').get('id')
@@ -150,7 +150,7 @@ def edit_catalog(catalog_id):
@document_bp.route('/processor', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def processor():
form = ProcessorForm()
@@ -179,7 +179,7 @@ def processor():
@document_bp.route('/processor/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_processor(processor_id):
"""Edit an existing processorr configuration."""
# Get the processor or return 404
@@ -228,7 +228,7 @@ def edit_processor(processor_id):
@document_bp.route('/processors', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def processors():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -247,7 +247,7 @@ def processors():
@document_bp.route('/handle_processor_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_processor_selection():
action = request.form['action']
if action == 'create_processor':
@@ -262,7 +262,7 @@ def handle_processor_selection():
@document_bp.route('/retriever', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def retriever():
form = RetrieverForm()
@@ -271,6 +271,8 @@ def retriever():
new_retriever = Retriever()
form.populate_obj(new_retriever)
new_retriever.catalog_id = form.catalog.data.id
+ new_retriever.type_version = cache_manager.retrievers_version_tree_cache.get_latest_version(
+ new_retriever.type)
set_logging_information(new_retriever, dt.now(tz.utc))
@@ -291,7 +293,7 @@ def retriever():
@document_bp.route('/retriever/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_retriever(retriever_id):
"""Edit an existing retriever configuration."""
# Get the retriever or return 404
@@ -341,7 +343,7 @@ def edit_retriever(retriever_id):
@document_bp.route('/retrievers', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def retrievers():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -360,7 +362,7 @@ def retrievers():
@document_bp.route('/handle_retriever_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_retriever_selection():
action = request.form['action']
if action == 'create_retriever':
@@ -375,7 +377,7 @@ def handle_retriever_selection():
@document_bp.route('/add_document', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def add_document():
form = AddDocumentForm(request.form)
catalog_id = session.get('catalog_id', None)
@@ -430,7 +432,7 @@ def add_document():
@document_bp.route('/add_url', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def add_url():
form = AddURLForm(request.form)
catalog_id = session.get('catalog_id', None)
@@ -489,14 +491,14 @@ def add_url():
@document_bp.route('/documents', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def documents():
view = DocumentListView(Document, 'document/documents.html', per_page=10)
return view.get()
@document_bp.route('/handle_document_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_document_selection():
document_identification = request.form['selected_row']
if isinstance(document_identification, int) or document_identification.isdigit():
@@ -527,7 +529,7 @@ def handle_document_selection():
@document_bp.route('/edit_document/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_document_view(document_id):
# Use an alias for the Catalog to avoid column name conflicts
CatalogAlias = aliased(Catalog)
@@ -568,7 +570,7 @@ def edit_document_view(document_id):
@document_bp.route('/edit_document_version/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_document_version_view(document_version_id):
doc_vers = DocumentVersion.query.get_or_404(document_version_id)
form = EditDocumentVersionForm(request.form, obj=doc_vers)
@@ -607,7 +609,7 @@ def edit_document_version_view(document_version_id):
@document_bp.route('/document_versions/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def document_versions(document_id):
doc = Document.query.get_or_404(document_id)
doc_desc = f'Document {doc.name}'
@@ -631,7 +633,7 @@ def document_versions(document_id):
@document_bp.route('/handle_document_version_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_document_version_selection():
document_version_identification = request.form['selected_row']
if isinstance(document_version_identification, int) or document_version_identification.isdigit():
@@ -658,13 +660,13 @@ def handle_document_version_selection():
@document_bp.route('/library_operations', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def library_operations():
return render_template('document/library_operations.html')
@document_bp.route('/handle_library_selection', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_library_selection():
action = request.form['action']
@@ -762,7 +764,7 @@ def create_default_rag_library():
@document_bp.route('/document_versions_list', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def document_versions_list():
view = DocumentVersionListView(DocumentVersion, 'document/document_versions_list_view.html', per_page=20)
return view.get()
diff --git a/eveai_app/views/entitlements_views.py b/eveai_app/views/entitlements_views.py
index b7fbc41..739fa4c 100644
--- a/eveai_app/views/entitlements_views.py
+++ b/eveai_app/views/entitlements_views.py
@@ -45,7 +45,7 @@ def license_tier():
@entitlements_bp.route('/view_license_tiers', methods=['GET', 'POST'])
-@roles_required('Super User')
+@roles_accepted('Super User')
def view_license_tiers():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -68,7 +68,7 @@ def view_license_tiers():
@entitlements_bp.route('/handle_license_tier_selection', methods=['POST'])
-@roles_required('Super User')
+@roles_accepted('Super User')
def handle_license_tier_selection():
action = request.form['action']
if action == 'create_license_tier':
@@ -214,7 +214,7 @@ def edit_license(license_id):
@entitlements_bp.route('/view_usages')
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_usages():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -236,7 +236,7 @@ def view_usages():
@entitlements_bp.route('/handle_usage_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_usage_selection():
usage_identification = request.form['selected_row']
usage_id = ast.literal_eval(usage_identification).get('value')
@@ -248,7 +248,7 @@ def handle_usage_selection():
@entitlements_bp.route('/view_licenses')
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_licenses():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -285,7 +285,7 @@ def view_licenses():
@entitlements_bp.route('/handle_license_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_license_selection():
license_identification = request.form['selected_row']
license_id = ast.literal_eval(license_identification).get('value')
diff --git a/eveai_app/views/interaction_views.py b/eveai_app/views/interaction_views.py
index 04726ca..05aa3b2 100644
--- a/eveai_app/views/interaction_views.py
+++ b/eveai_app/views/interaction_views.py
@@ -66,7 +66,7 @@ def chat_sessions():
@interaction_bp.route('/handle_chat_session_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_chat_session_selection():
chat_session_identification = request.form['selected_row']
cs_id = ast.literal_eval(chat_session_identification).get('value')
@@ -82,7 +82,7 @@ def handle_chat_session_selection():
@interaction_bp.route('/view_chat_session/', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_chat_session(chat_session_id):
# Get chat session with user info
chat_session = ChatSession.query.get_or_404(chat_session_id)
@@ -122,7 +122,7 @@ def view_chat_session(chat_session_id):
@interaction_bp.route('/view_chat_session_by_session_id/', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_chat_session_by_session_id(session_id):
chat_session = ChatSession.query.filter_by(session_id=session_id).first_or_404()
show_chat_session(chat_session)
@@ -135,7 +135,7 @@ def show_chat_session(chat_session):
# Routes for Specialist Management ----------------------------------------------------------------
@interaction_bp.route('/specialist', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialist():
form = SpecialistForm()
@@ -185,7 +185,7 @@ def specialist():
@interaction_bp.route('/specialist/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_specialist(specialist_id):
specialist = Specialist.query.get_or_404(specialist_id)
form = EditSpecialistForm(request.form, obj=specialist)
@@ -273,7 +273,7 @@ def edit_specialist(specialist_id):
@interaction_bp.route('/specialists', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def specialists():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -292,7 +292,7 @@ def specialists():
@interaction_bp.route('/handle_specialist_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_specialist_selection():
action = request.form.get('action')
if action == 'create_specialist':
@@ -309,7 +309,7 @@ def handle_specialist_selection():
# Routes for Agent management ---------------------------------------------------------------------
@interaction_bp.route('/agent//edit', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_agent(agent_id):
agent = EveAIAgent.query.get_or_404(agent_id)
form = EditEveAIAgentForm(obj=agent)
@@ -325,7 +325,7 @@ def edit_agent(agent_id):
@interaction_bp.route('/agent//save', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def save_agent(agent_id):
agent = EveAIAgent.query.get_or_404(agent_id) if agent_id else EveAIAgent()
tenant_id = session.get('tenant').get('id')
@@ -349,7 +349,7 @@ def save_agent(agent_id):
# Routes for Task management ----------------------------------------------------------------------
@interaction_bp.route('/task//edit', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_task(task_id):
task = EveAITask.query.get_or_404(task_id)
form = EditEveAITaskForm(obj=task)
@@ -361,7 +361,7 @@ def edit_task(task_id):
@interaction_bp.route('/task//save', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def save_task(task_id):
task = EveAITask.query.get_or_404(task_id) if task_id else EveAITask()
tenant_id = session.get('tenant').get('id')
@@ -385,7 +385,7 @@ def save_task(task_id):
# Routes for Tool management ----------------------------------------------------------------------
@interaction_bp.route('/tool//edit', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_tool(tool_id):
tool = EveAITool.query.get_or_404(tool_id)
form = EditEveAIToolForm(obj=tool)
@@ -397,7 +397,7 @@ def edit_tool(tool_id):
@interaction_bp.route('/tool//save', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def save_tool(tool_id):
tool = EveAITool.query.get_or_404(tool_id) if tool_id else EveAITool()
tenant_id = session.get('tenant').get('id')
@@ -421,7 +421,7 @@ def save_tool(tool_id):
# Component selection handlers --------------------------------------------------------------------
@interaction_bp.route('/handle_agent_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_agent_selection():
agent_identification = request.form['selected_row']
agent_id = ast.literal_eval(agent_identification).get('value')
@@ -434,7 +434,7 @@ def handle_agent_selection():
@interaction_bp.route('/handle_task_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_task_selection():
task_identification = request.form['selected_row']
task_id = ast.literal_eval(task_identification).get('value')
@@ -447,7 +447,7 @@ def handle_task_selection():
@interaction_bp.route('/handle_tool_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_tool_selection():
tool_identification = request.form['selected_row']
tool_id = ast.literal_eval(tool_identification).get('value')
@@ -461,7 +461,7 @@ def handle_tool_selection():
# Routes for Asset management ---------------------------------------------------------------------
@interaction_bp.route('/add_asset', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def add_asset():
form = AddEveAIAssetForm(request.form)
tenant_id = session.get('tenant').get('id')
@@ -489,7 +489,7 @@ def add_asset():
@interaction_bp.route('/edit_asset_version/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_asset_version(asset_version_id):
asset_version = EveAIAssetVersion.query.get_or_404(asset_version_id)
form = EditEveAIAssetVersionForm(asset_version)
diff --git a/eveai_app/views/security_views.py b/eveai_app/views/security_views.py
index 2ed030b..a8088bd 100644
--- a/eveai_app/views/security_views.py
+++ b/eveai_app/views/security_views.py
@@ -11,7 +11,7 @@ from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
from common.models.user import User
-from common.utils.eveai_exceptions import EveAIException
+from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense
from common.utils.nginx_utils import prefixed_url_for
from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, RequestResetForm
from common.extensions import db
diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py
index c2e2635..d6033b9 100644
--- a/eveai_app/views/user_forms.py
+++ b/eveai_app/views/user_forms.py
@@ -1,9 +1,10 @@
-from flask import current_app
+from flask import current_app, session
from flask_wtf import FlaskForm
from wtforms import (StringField, PasswordField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
SelectField, SelectMultipleField, FieldList, FormField, FloatField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
import pytz
+from flask_security import current_user
from common.models.user import Role
from common.services.user_service import UserService
@@ -24,6 +25,9 @@ class TenantForm(FlaskForm):
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
# LLM fields
llm_model = SelectField('Large Language Model', choices=[], validators=[DataRequired()])
+
+ # For Super Users only - Allow to assign the tenant to the partner
+ assign_to_partner = BooleanField('Assign to Partner', default=False)
# Embedding variables
submit = SubmitField('Submit')
@@ -40,6 +44,9 @@ class TenantForm(FlaskForm):
self.llm_model.choices = [(model, model) for model in current_app.config['SUPPORTED_LLMS']]
# Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
+ # Show field only for Super Users with partner in session
+ if not current_user.has_roles('Super User') or 'partner' not in session:
+ self._fields.pop('assign_to_partner', None)
class BaseUserForm(FlaskForm):
diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py
index 95cb181..42d3d00 100644
--- a/eveai_app/views/user_views.py
+++ b/eveai_app/views/user_views.py
@@ -1,14 +1,12 @@
-# from . import user_bp
import uuid
from datetime import datetime as dt, timezone as tz
-from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
+from flask import request, redirect, flash, render_template, Blueprint, session, current_app
from flask_mailman import EmailMessage
-from flask_security import hash_password, roles_required, roles_accepted, current_user
-from itsdangerous import URLSafeTimedSerializer
+from flask_security import roles_accepted, current_user
from sqlalchemy.exc import SQLAlchemyError
import ast
-from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, Partner
+from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
from common.extensions import db, security, minio_client, simple_encryption
from common.services.user_service import UserService
from common.utils.security_utils import send_confirmation_email, send_reset_email
@@ -19,8 +17,9 @@ from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key
from common.utils.nginx_utils import prefixed_url_for
-from common.utils.eveai_exceptions import EveAIDoublePartner, EveAIException
+from common.utils.eveai_exceptions import EveAIException
from common.utils.document_utils import set_logging_information, update_logging_information
+from common.services.tenant_service import TenantService
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -36,7 +35,7 @@ def log_after_request(response):
@user_bp.route('/tenant', methods=['GET', 'POST'])
-@roles_required('Super User')
+@roles_accepted('Super User', 'Partner Admin')
def tenant():
form = TenantForm()
if request.method == 'GET':
@@ -48,7 +47,6 @@ def tenant():
new_tenant = Tenant()
form.populate_obj(new_tenant)
- # Handle Timestamps
timestamp = dt.now(tz.utc)
new_tenant.created_at = timestamp
new_tenant.updated_at = timestamp
@@ -57,11 +55,24 @@ def tenant():
try:
db.session.add(new_tenant)
db.session.commit()
+
+ if current_user.has_roles('Partner Admin') and 'partner' in session:
+ # Always associate with the partner for Partner Admins
+ TenantService.associate_tenant_with_partner(new_tenant.id)
+ elif current_user.has_roles('Super User') and form.assign_to_partner.data and 'partner' in session:
+ # Super User chose to associate with partner
+ TenantService.associate_tenant_with_partner(new_tenant.id)
+
except SQLAlchemyError as e:
current_app.logger.error(f'Failed to add tenant to database. Error: {str(e)}')
flash(f'Failed to add tenant to database. Error: {str(e)}', 'danger')
return render_template('user/tenant.html', form=form)
+ except EveAIException as e:
+ current_app.logger.error(f'Error associating Tenant {new_tenant.id} to Partner. Error: {str(e)}')
+ flash(f'Error associating Tenant to Partner. Error: {str(e)}', 'danger')
+ return render_template('user/tenant.html', form=form)
+
current_app.logger.info(f"Successfully created tenant {new_tenant.id} in Database")
flash(f"Successfully created tenant {new_tenant.id} in Database", 'success')
@@ -81,15 +92,11 @@ def tenant():
@user_bp.route('/tenant/', methods=['GET', 'POST'])
-@roles_required('Super User')
+@roles_accepted('Super User', 'Partner Admin')
def edit_tenant(tenant_id):
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found
form = TenantForm(obj=tenant)
- if request.method == 'GET':
- # Populate the form with tenant data
- form.populate_obj(tenant)
-
if form.validate_on_submit():
# Populate the tenant with form data
form.populate_obj(tenant)
@@ -109,6 +116,7 @@ def edit_tenant(tenant_id):
@user_bp.route('/user', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin', 'Partner Admin')
def user():
+ tenant_id = session.get('tenant').get('id')
form = CreateUserForm()
form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant
if form.validate_on_submit():
@@ -141,7 +149,7 @@ def user():
try:
send_confirmation_email(new_user)
current_app.logger.info(f'User {new_user.id} with name {new_user.user_name} added to database'
- f'Confirmation email sent to {new_user.email}')
+ f'Confirmation email sent to {new_user.email}')
flash('User added successfully and confirmation email sent.', 'success')
except Exception as e:
current_app.logger.error(f'Failed to send confirmation email to {new_user.email}. Error: {str(e)}')
@@ -205,14 +213,40 @@ def edit_user(user_id):
@user_bp.route('/select_tenant', methods=['GET', 'POST'])
-@roles_required('Super User')
+@roles_accepted('Super User', 'Partner Admin') # Allow both roles
def select_tenant():
filter_form = TenantSelectionForm(request.form)
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
+ # Start with a base query
query = Tenant.query
+ # Apply different filters based on user role
+ if current_user.has_roles('Partner Admin') and 'partner' in session:
+ # Get the partner's management service
+ management_service = next((service for service in session['partner']['services']
+ if service.get('type') == 'MANAGEMENT_SERVICE'), None)
+
+ if management_service:
+ # Get the partner's own tenant
+ partner_tenant_id = session['partner']['tenant_id']
+
+ # Get tenants managed by this partner through PartnerTenant relationships
+ managed_tenant_ids = db.session.query(PartnerTenant.tenant_id).filter_by(
+ partner_service_id=management_service['id']
+ ).all()
+
+ # Convert list of tuples to flat list
+ managed_tenant_ids = [tenant_id for (tenant_id,) in managed_tenant_ids]
+
+ # Include partner's own tenant in the list
+ allowed_tenant_ids = [partner_tenant_id] + managed_tenant_ids
+
+ # Filter query to only show allowed tenants
+ query = query.filter(Tenant.id.in_(allowed_tenant_ids))
+
+ # Apply form filters (for both Super User and Partner Admin)
if filter_form.validate_on_submit():
if filter_form.types.data:
query = query.filter(Tenant.type.in_(filter_form.types.data))
@@ -220,6 +254,7 @@ def select_tenant():
search = f"%{filter_form.search.data}%"
query = query.filter(Tenant.name.ilike(search))
+ # Finalize query
query = query.order_by(Tenant.name)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
tenants = pagination.items
@@ -230,7 +265,7 @@ def select_tenant():
@user_bp.route('/handle_tenant_selection', methods=['POST'])
-@roles_required('Super User')
+@roles_accepted('Super User', 'Partner Admin')
def handle_tenant_selection():
action = request.form['action']
if action == 'create_tenant':
@@ -238,6 +273,10 @@ def handle_tenant_selection():
tenant_identification = request.form['selected_row']
tenant_id = ast.literal_eval(tenant_identification).get('value')
+ if not TenantService.can_user_edit_tenant(tenant_id):
+ current_app.logger.info(f"User not authenticated to edit tenant {tenant_id}.")
+ flash(f"You are not authenticated to manage tenant {tenant_id}", 'danger')
+ return redirect(prefixed_url_for('select_tenant'))
the_tenant = Tenant.query.get(tenant_id)
# set tenant information in the session
@@ -259,7 +298,7 @@ def handle_tenant_selection():
@user_bp.route('/view_users')
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_users():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -279,7 +318,7 @@ def view_users():
@user_bp.route('/handle_user_action', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_user_action():
action = request.form['action']
if action == 'create_user':
@@ -305,7 +344,7 @@ def handle_user_action():
@user_bp.route('/view_tenant_domains')
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_tenant_domains():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -324,7 +363,7 @@ def view_tenant_domains():
@user_bp.route('/handle_tenant_domain_action', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_tenant_domain_action():
action = request.form['action']
if action == 'create_tenant_domain':
@@ -340,7 +379,7 @@ def handle_tenant_domain_action():
@user_bp.route('/tenant_domain', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_domain():
form = TenantDomainForm()
if form.validate_on_submit():
@@ -354,7 +393,8 @@ def tenant_domain():
db.session.add(new_tenant_domain)
db.session.commit()
flash('Tenant Domain added successfully.', 'success')
- current_app.logger.info(f'Tenant Domain {new_tenant_domain.domain} added for tenant {session["tenant"]["id"]}')
+ current_app.logger.info(
+ f'Tenant Domain {new_tenant_domain.domain} added for tenant {session["tenant"]["id"]}')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to add Tenant Domain. Error: {str(e)}', 'danger')
@@ -368,7 +408,7 @@ def tenant_domain():
@user_bp.route('/tenant_domain/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_tenant_domain(tenant_domain_id):
tenant_domain = TenantDomain.query.get_or_404(tenant_domain_id) # This will return a 404 if no user is found
form = TenantDomainForm(obj=tenant_domain)
@@ -396,7 +436,7 @@ def edit_tenant_domain(tenant_domain_id):
@user_bp.route('/tenant_overview', methods=['GET'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_overview():
tenant_id = session['tenant']['id']
tenant = Tenant.query.get_or_404(tenant_id)
@@ -405,7 +445,7 @@ def tenant_overview():
@user_bp.route('/tenant_project', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_project():
form = TenantProjectForm()
if request.method == 'GET':
@@ -458,7 +498,7 @@ def tenant_project():
@user_bp.route('/tenant_projects', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def tenant_projects():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
@@ -478,7 +518,7 @@ def tenant_projects():
@user_bp.route('/handle_tenant_project_selection', methods=['POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def handle_tenant_project_selection():
action = request.form.get('action')
if action == 'create_tenant_project':
@@ -508,8 +548,8 @@ def handle_tenant_project_selection():
return redirect(prefixed_url_for('user_bp.tenant_projects'))
-@user_bp.route('/tenant_project/', methods=['GET','POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@user_bp.route('/tenant_project/', methods=['GET', 'POST'])
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def edit_tenant_project(tenant_project_id):
tenant_project = TenantProject.query.get_or_404(tenant_project_id)
tenant_id = session['tenant']['id']
@@ -535,7 +575,7 @@ def edit_tenant_project(tenant_project_id):
@user_bp.route('/tenant_project/delete/', methods=['GET', 'POST'])
-@roles_accepted('Super User', 'Tenant Admin')
+@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def delete_tenant_project(tenant_project_id):
tenant_id = session['tenant']['id']
tenant_project = TenantProject.query.get_or_404(tenant_project_id)
@@ -570,18 +610,6 @@ def reset_uniquifier(user):
send_reset_email(user)
-def set_logging_information(obj, timestamp):
- obj.created_at = timestamp
- obj.updated_at = timestamp
- obj.created_by = current_user.id
- obj.updated_by = current_user.id
-
-
-def update_logging_information(obj, timestamp):
- obj.updated_at = timestamp
- obj.updated_by = current_user.id
-
-
def get_notification_email(tenant_id, user_email=None):
"""
Determine which email address to use for notification.