From 5500c6c39c2afd3f6b4e49837286a223eed4f93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oravecz=20Tam=C3=A1s?= Date: Wed, 29 Nov 2023 18:07:45 +0100 Subject: [PATCH] support navigation into app,dinamic email sender, basic request logger --- analogic/__init__.py | 1 + analogic/analogic.py | 8 +-- analogic/authentication_provider.py | 64 ++++++++++++++++++++ analogic/cam.py | 5 ++ analogic/core_endpoints.py | 31 +++++++++- analogic/email.py | 5 +- analogic/logging.json | 50 +++++++++++++++ analogic/pivot.py | 2 +- analogic/request_logger.py | 13 ++++ analogic/setting.py | 10 +++ analogic/static/assets/js/framework/app.js | 17 +++++- analogic/static/assets/js/framework/utils.js | 7 +++ 12 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 analogic/request_logger.py diff --git a/analogic/__init__.py b/analogic/__init__.py index f90e022e..792e82b2 100644 --- a/analogic/__init__.py +++ b/analogic/__init__.py @@ -32,3 +32,4 @@ from .signal_receiver import SignalReceiver from .default_signal_receiver import DefaultSignalReceiver from .long_running_task_executor import LongRunningTaskExecutor +from .request_logger import RequestLogger diff --git a/analogic/analogic.py b/analogic/analogic.py index 8225e22b..6132ee69 100644 --- a/analogic/analogic.py +++ b/analogic/analogic.py @@ -246,10 +246,10 @@ def _load_logging(app): log_config = json.load(file) for h in log_config['handlers']: if 'filename' in log_config['handlers'][h]: - if h == 'scheduler_file_handler': - file_name = f"logs/scheduler_{os.getpid()}.log" - else: - file_name = log_config['handlers'][h]['filename'] + # if h == 'scheduler_file_handler': + # file_name = f"logs/scheduler_{os.getpid()}.log" + # else: + file_name = log_config['handlers'][h]['filename'] log_config['handlers'][h]['filename'] = os.path.join(app.instance_path, file_name) diff --git a/analogic/authentication_provider.py b/analogic/authentication_provider.py index 48060253..b93f51fe 100644 --- a/analogic/authentication_provider.py +++ b/analogic/authentication_provider.py @@ -4,6 +4,8 @@ from analogic.exceptions import AnalogicProxyException, AnalogicAccessDeniedException, AnalogicException, \ AnalogicAcceptedException from analogic.session_handler import SessionHandler +from analogic.request_logger import RequestLogger +from analogic.setting import SettingManager import logging import pandas as pd from abc import ABC, abstractmethod @@ -67,6 +69,7 @@ def __init__(self, setting): self.logged_in_user_session_name = '_logged_in_user_name' self._logger = logging.getLogger(self.setting.get_instance()) self.session_handler = SessionHandler(self.setting.get_instance_and_name()) + self.request_logger = RequestLogger() self.user_subscriptions = {} def initialize(self): @@ -78,6 +81,15 @@ def get_setting(self): def get_logged_in_user_name(self): return self.session_handler.get(self.logged_in_user_session_name) + def set_navigation_parameters(self, value): + self.session_handler.set('navigation_parameters', value) + + def get_navigation_parameters(self): + return self.session_handler.get('navigation_parameters') + + def clear_navigation_parameters(self): + self.session_handler.delete('navigation_parameters') + @login_required def pivot(self): username = self.get_logged_in_user_name() @@ -141,16 +153,21 @@ def middleware(self): def _get_check_access_mdx(self, force_server_side_query=False): if request.args.get('server') is not None or force_server_side_query is True: + if request.method == 'GET': body = request.args elif len(request.form) > 0: body = request.form.to_dict() else: body = orjson.loads(request.data) + key = body.get('key') + if body.get('key_suffix') is not None: key = key + '_' + body['key_suffix'] + key = key + self.get_setting().get_check_access_repository_yml_suffix() + mdx = self.setting.get_mdx(key) if mdx is None: @@ -162,34 +179,73 @@ def _get_check_access_mdx(self, force_server_side_query=False): if type(body[k]) is not dict: mdx = mdx.replace('$' + k, body[k].replace('"', '\\"')) + self.log_request(**body) + return mdx.encode('utf-8') return None + def log_request(self, **kwargs): + if self.setting.is_request_logger_enabled() is True: + params = kwargs.copy() + + params['logged_in_user'] = self.get_logged_in_user_name() + + params.update(self.get_additional_log_request_parameters()) + + self.request_logger.log_request(**params) + + def log_write_request(self, **kwargs): + if self.setting.is_write_request_logger_enabled() is True and self.is_write_request(): + params = kwargs.copy() + + params['logged_in_user'] = self.get_logged_in_user_name() + + params.update(self.get_additional_log_request_parameters()) + + self.request_logger.log_write_request(**params) + + def get_additional_log_request_parameters(self): + return {} + + def is_write_request(self): + return 'tm1.ExecuteWithReturn' in request.full_path + def _get_server_side_mdx(self, force_server_side_query=False): + if request.args.get('server') is not None or force_server_side_query is True: + if request.method == 'GET': body = request.args elif len(request.form) > 0: body = request.form.to_dict() else: body = orjson.loads(request.data) + key = body['key'] + if body.get('key_suffix') is not None: key = key + '_' + body['key_suffix'] + mdx = self.setting.get_mdx(key) if isinstance(mdx, dict): + if 'required_permissions' in mdx and self.check_permission(mdx.get('required_permissions')) is False: raise AnalogicAccessDeniedException('You do not have access to run the query {}'.format(key)) mdx = mdx['query'] mdx = self._set_custom_mdx_data(mdx) + for k in body: + if type(body[k]) is not dict: mdx = mdx.replace('$' + k, body[k].replace('"', '\\"')) + self.log_request(**body) + self.log_write_request(**body) + return mdx.encode('utf-8') else: return ''.encode('utf-8') @@ -442,6 +498,14 @@ def get_setting_parameter_descriptions(): 'sessionExpiresInMinutes': { 'required': False, 'description': 'Numeric value. Session will expire after the added value. Default 20' + }, + SettingManager.ENABLE_REQUEST_LOGGER_PARAMETER_NAME: { + 'required': False, + 'description': 'Boolean value, default false. All of the request parameter will be added to the log' + }, + SettingManager.ENABLE_WRITE_REQUEST_LOGGER_PARAMETER_NAME: { + 'required': False, + 'description': 'Boolean value, default false. All of the write request parameter will be added to the log' } } diff --git a/analogic/cam.py b/analogic/cam.py index fef41e67..08e73007 100644 --- a/analogic/cam.py +++ b/analogic/cam.py @@ -173,6 +173,11 @@ def get_tm1_service(self): def _extend_login_session(self): session.modified = True + def _set_custom_mdx_data(self, mdx): + if mdx is not None and len(mdx) > 0: + return mdx.replace('$activeUser', self.get_logged_in_user_name()) + return mdx + @staticmethod def get_setting_parameter_descriptions(): result = super(Cam, Cam).get_setting_parameter_descriptions() diff --git a/analogic/core_endpoints.py b/analogic/core_endpoints.py index 62ff3e51..137c74f7 100644 --- a/analogic/core_endpoints.py +++ b/analogic/core_endpoints.py @@ -1,13 +1,38 @@ from analogic.endpoint import AnalogicEndpoint from analogic.authentication_provider import get_authentication_provider - +from flask import request, redirect +import base64 core_endpoints = AnalogicEndpoint('core_endpoints', __name__) @core_endpoints.analogic_endpoint_route('/', methods=['GET', 'POST']) def index(): - return get_authentication_provider().index() + authentication_provider = get_authentication_provider() + + navigation_parameters = request.args.get('p') + + if navigation_parameters is not None: + authentication_provider.set_navigation_parameters(navigation_parameters) + return redirect(authentication_provider.setting.get_base_url()) + + response = authentication_provider.index() + + return response + + +@core_endpoints.analogic_endpoint_route('/navigation_parameters', methods=['GET']) +def navigation_parameters(): + authentication_provider = get_authentication_provider() + + navigation_parameters = authentication_provider.get_navigation_parameters() + + if navigation_parameters is not None: + + authentication_provider.clear_navigation_parameters() + + return {'navigation_parameters': navigation_parameters}, 200, {'Content-type': 'application/json'} + @core_endpoints.analogic_endpoint_route('/login', methods=['GET', 'POST']) @@ -44,6 +69,7 @@ def export(): def clear_cache(): return get_authentication_provider().clear_cache() + @core_endpoints.analogic_endpoint_route('/pivot', methods=['GET', 'POST']) def pivot(): return get_authentication_provider().pivot() @@ -52,4 +78,3 @@ def pivot(): @core_endpoints.analogic_endpoint_route('/middleware', methods=['GET', 'POST']) def middleware(): return get_authentication_provider().middleware() - diff --git a/analogic/email.py b/analogic/email.py index 623fe063..e0f7931e 100644 --- a/analogic/email.py +++ b/analogic/email.py @@ -43,7 +43,10 @@ def send_email_by_params(setting, post_data): smtp_config = cnf['smtp'] - sender_email = EmailManager._get_required_config_value(smtp_config, 'sender_email') + if post_data.get('sender_email') is None: + sender_email = EmailManager._get_required_config_value(smtp_config, 'sender_email') + else: + sender_email = post_data.get('sender_email') receiver_email = post_data.get('receiver_email') diff --git a/analogic/logging.json b/analogic/logging.json index d18598bf..5acf5e1b 100644 --- a/analogic/logging.json +++ b/analogic/logging.json @@ -102,6 +102,46 @@ "encoding": "utf8" }, + "request_logger_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "logs/request.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + + "request_logger_json_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "json", + "filename": "logs/request_json.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + + "write_request_logger_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "logs/write_request.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + + "write_request_logger_json_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "json", + "filename": "logs/write_request_json.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + "console": { "class": "logging.StreamHandler", "level": "DEBUG", @@ -114,6 +154,16 @@ "level": "INFO", "handlers": ["login_file_handler", "login_json_file_handler"], "propagate": false + }, + "request_logger": { + "level": "INFO", + "handlers": ["request_logger_file_handler", "request_logger_json_file_handler"], + "propagate": false + }, + "write_request_logger": { + "level": "INFO", + "handlers": ["write_request_logger_file_handler", "write_request_logger_json_file_handler"], + "propagate": false }, "werkzeug": { "level": "INFO", diff --git a/analogic/pivot.py b/analogic/pivot.py index f019d2c1..1aab3ac6 100644 --- a/analogic/pivot.py +++ b/analogic/pivot.py @@ -7,7 +7,7 @@ from TM1py.Objects import Subset from TM1py.Services import TM1Service from TM1py.Utils import format_url -from flask send_file +from flask import send_file def call(tm1: TM1Service, username, cube_name=None, dimension_name=None, hierarchy_name=None, subset_name=None, element_names=None, subset_name_to_remove=None, selected_cards=None, options=None, export_data=None): diff --git a/analogic/request_logger.py b/analogic/request_logger.py new file mode 100644 index 00000000..e8d47f42 --- /dev/null +++ b/analogic/request_logger.py @@ -0,0 +1,13 @@ +import logging + +class RequestLogger: + + def __init__(self): + self.request_logger = logging.getLogger('request_logger') + self.write_request_logger = logging.getLogger('write_request_logger') + + def log_request(self, **kwargs): + self.request_logger.info(kwargs) + + def log_write_request(self, **kwargs): + self.write_request_logger.info(kwargs) diff --git a/analogic/setting.py b/analogic/setting.py index b4c72933..26691a89 100644 --- a/analogic/setting.py +++ b/analogic/setting.py @@ -9,6 +9,10 @@ class SettingManager: + ENABLE_REQUEST_LOGGER_PARAMETER_NAME = '_enable_request_logger' + ENABLE_WRITE_REQUEST_LOGGER_PARAMETER_NAME = '_enable_write_request_logger' + + def __init__(self, analogic_application_path, instance='default'): self.site_root = analogic_application_path self.instance = instance @@ -173,5 +177,11 @@ def get_permission_query_repository_yml_key(self): def get_permission_session_name(self): return self.get_instance() + '_analogic_permissions' + def is_request_logger_enabled(self): + return self.get_config().get(self.ENABLE_REQUEST_LOGGER_PARAMETER_NAME, False) + + def is_write_request_logger_enabled(self): + return self.get_config().get(self.ENABLE_WRITE_REQUEST_LOGGER_PARAMETER_NAME, False) + def getLogger(self): return self._logger diff --git a/analogic/static/assets/js/framework/app.js b/analogic/static/assets/js/framework/app.js index c8b8634b..df3e966c 100644 --- a/analogic/static/assets/js/framework/app.js +++ b/analogic/static/assets/js/framework/app.js @@ -97,7 +97,22 @@ window.onerror = (msg, url, lineNum, colNum, error) => { Widgets.systemValueGlobalCompanyProductPlanVersion = 'Budget'; - Widgets[app.mainPage].renderWidget().then(() => Utils.checkScreenResolution()); + Auth.getAjaxRequest('navigation_parameters', {}, 'GET').then((data) => { + let page = app.mainPage; + if (data.navigation_parameters) { + let navigation_parameters = JSON.parse(atob(data.navigation_parameters)); + + if (navigation_parameters.page) { + page = navigation_parameters.page; + } + for(let key in navigation_parameters) { + if (key !== 'page') { + Widgets[key] = navigation_parameters[key]; + } + } + } + Widgets[page].renderWidget().then(() => Utils.checkScreenResolution()); + }); } diff --git a/analogic/static/assets/js/framework/utils.js b/analogic/static/assets/js/framework/utils.js index e0865eb7..e43705b9 100644 --- a/analogic/static/assets/js/framework/utils.js +++ b/analogic/static/assets/js/framework/utils.js @@ -570,6 +570,13 @@ const Utils = { }, undefinedOrFalse(val) { return typeof val === 'undefined' || val === false; + }, + getNavigationUrl(params) { + let url = window.location.href.split('?')[0]; + if (params) { + url += '?p=' + btoa(JSON.stringify(params)); + } + return url; } };