- Add 'Partner Admin' role to actual functionality in eveai_app

This commit is contained in:
Josako
2025-04-15 17:12:46 +02:00
parent 3eed546879
commit 5f58417d24
12 changed files with 281 additions and 135 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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()

View File

@@ -69,38 +69,38 @@
<ul class="navbar-nav navbar-nav-hover mx-auto">
{% 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 %}
</a>
</li>
{% if current_user.has_roles('Super User') and 'partner' in session %}
{% if 'partner' in session %}
<li class="nav-item mt-2">
<a href="/session_defaults" class="btn btn-sm bg-gradient-success mb-0">
PARTNER {{ session['partner'].get('id', 'None') }}: {{ session['partner'].get('name', 'None') }}

View File

@@ -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

View File

@@ -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/<int:catalog_id>', 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/<int:processor_id>', 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/<int:retriever_id>', 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/<int:document_id>', 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/<int:document_version_id>', 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/<int:document_id>', 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()

View File

@@ -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')

View File

@@ -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/<int:chat_session_id>', 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/<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/<int:specialist_id>', 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/<int:agent_id>/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/<int:agent_id>/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/<int:task_id>/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/<int:task_id>/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/<int:tool_id>/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/<int:tool_id>/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/<int:asset_version_id>', 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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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/<int:tenant_id>', 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/<int:tenant_domain_id>', 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/<int:tenant_project_id>', methods=['GET','POST'])
@roles_accepted('Super User', 'Tenant Admin')
@user_bp.route('/tenant_project/<int:tenant_project_id>', 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/<int:tenant_project_id>', 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.