From 24d4f8e5b77137dbca62fcec8cb2fc211966ce9b Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Mon, 12 Feb 2024 12:41:09 -0500 Subject: [PATCH] Refs #1. Added database model and started work on user API. --- API/user/QueryDashboard.py | 119 ++++++++++++++++++++++ DashboardsService.py | 2 +- FlaskModule.py | 9 +- libDashboards/db/DBManager.py | 9 +- libDashboards/db/models/DashDashboards.py | 102 +++++++++++++++++++ libDashboards/db/models/__init__.py | 6 ++ 6 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 API/user/QueryDashboard.py create mode 100644 libDashboards/db/models/DashDashboards.py create mode 100644 libDashboards/db/models/__init__.py diff --git a/API/user/QueryDashboard.py b/API/user/QueryDashboard.py new file mode 100644 index 0000000..118c481 --- /dev/null +++ b/API/user/QueryDashboard.py @@ -0,0 +1,119 @@ +from flask import request +from flask_restx import Resource, inputs +from sqlalchemy import exc, inspect + +from FlaskModule import user_api_ns as api +from libDashboards.db.models.DashDashboards import DashDashboards +from opentera.services.ServiceAccessManager import ServiceAccessManager, current_login_type, LoginType, \ + current_user_client +from flask_babel import gettext + + +# Parser definition(s) +# GET +get_parser = api.parser() +get_parser.add_argument('uuid', type=str, help='Specific dashboard uuid to query information for.') +get_parser.add_argument('id_site', type=int, help='ID of the site to query all dashboards for') +get_parser.add_argument('id_project', type=int, help='ID of the project to query all dashboards for') +get_parser.add_argument('globals', type=inputs.boolean, help='Query globals dashboards') + +get_parser.add_argument('all_versions', type=inputs.boolean, help='Return all versions of the dashboard(s)') +get_parser.add_argument('list', type=inputs.boolean, help='Return minimal information (to display in a list, for ' + 'example)') + +# POST +post_schema = api.schema_model('criteria', {'properties': DashDashboards.get_json_schema(), 'type': 'object', + 'location': 'json'}) + +# DELETE +delete_parser = api.parser() +delete_parser.add_argument('id', type=int, help='ID to delete') + + +class QueryDashboard(Resource): + + def __init__(self, _api, *args, **kwargs): + Resource.__init__(self, _api, *args, **kwargs) + self.module = kwargs.get('flaskModule', None) + self.test = kwargs.get('test', False) + + @api.doc(description='Get dashboard information. Should specify only one id or the "globals" parameter', + responses={200: 'Success - returns list of dashboards', + 400: 'Required parameter is missing', + 403: 'Logged user doesn\'t have permission to access the requested data'}, + params={'token': 'Secret token'}) + @api.expect(get_parser) + @ServiceAccessManager.token_required(allow_static_tokens=False, allow_dynamic_tokens=True) + def get(self): + if current_login_type != LoginType.USER_LOGIN: + return gettext('Only users can use this API.'), 403 + + # Parse arguments + request_args = get_parser.parse_args(strict=False) + + dashboards = [] + if 'uuid' in request_args: + dashboard = DashDashboards.get_dashboard_by_uuid(request_args['uuid']) + if not dashboard: + return gettext('Forbidden'), 403 # Explicitely vague for security purpose + + if dashboard.id_site: + site_role = current_user_client.get_role_for_site(dashboard.id_site) + if site_role == 'Undefined': + return gettext('Forbidden'), 403 + if dashboard.id_project: + project_role = current_user_client.get_role_for_project(dashboard.id_project) + if project_role == 'Undefined': + return gettext('Forbidden'), 403 + + if not dashboard.id_project and not dashboard.id_site: + # Global dashboard - only for super admins + if not current_user_client.user_superadmin: + return gettext('Forbidden'), 403 + dashboards = [dashboard] + + elif 'id_site' in request_args: + site_role = current_user_client.get_role_for_site(request_args['id_site']) + if site_role == 'Undefined': + return gettext('Forbidden'), 403 + dashboards = DashDashboards.get_dashboards_for_site(request_args['id_site']) + + elif 'id_project' in request_args: + project_role = current_user_client.get_role_for_project(request_args['id_project']) + if project_role == 'Undefined': + return gettext('Forbidden'), 403 + dashboards = DashDashboards.get_dashboards_for_project(request_args['id_project']) + + elif request_args['globals']: + if not current_user_client.user_superadmin: + return gettext('Forbidden'), 403 + dashboards = DashDashboards.get_dashboards_globals() + else: + return gettext('Must specify at least one id parameter or "globals"') + + # Convert to json and return + dashboards_json = [dash.to_json(request_args['list']) for dash in dashboards] + return dashboards_json + + @api.expect(post_schema) + @api.doc(description='Create or update a dashboard', + responses={200: 'Success', + 403: 'No access to this API', + 400: 'Missing parameter in request' + }, + params={'token': 'Secret token'}) + @ServiceAccessManager.token_required(allow_static_tokens=False, allow_dynamic_tokens=True) + def post(self): + if current_login_type != LoginType.USER_LOGIN: + return gettext('Only users can use this API.'), 403 + + @api.expect(delete_parser, validate=True) + @api.doc(description='Delete a dashboard', + responses={200: 'Success - deleted', + 400: 'Bad request', + 403: 'Access denied'}, + params={'token': 'Secret token'}) + @ServiceAccessManager.token_required(allow_static_tokens=False, allow_dynamic_tokens=True) + def delete(self): + if current_login_type != LoginType.USER_LOGIN: + return gettext('Only users can use this API.'), 403 diff --git a/DashboardsService.py b/DashboardsService.py index 224ba3a..a244ebf 100644 --- a/DashboardsService.py +++ b/DashboardsService.py @@ -66,7 +66,6 @@ def notify_service_messages(self, pattern, channel, message): 'correctly set in this service?') sys.exit(1) - service_info = json.loads(service_info) if 'service_uuid' not in service_info: sys.stderr.write('OpenTera Server didn\'t return a valid service UUID - aborting.') @@ -94,6 +93,7 @@ def notify_service_messages(self, pattern, channel, message): Globals.db_man.open_local(None, echo=True) else: Globals.db_man.open(POSTGRES, Globals.config_man.service_config['debug_mode']) + Globals.db_man.create_defaults(True) except OperationalError as e: print("Unable to connect to database - please check settings in config file!", e) quit() diff --git a/FlaskModule.py b/FlaskModule.py index 10542c4..f389f5c 100644 --- a/FlaskModule.py +++ b/FlaskModule.py @@ -141,7 +141,7 @@ def base_path(self): } # API -api = CustomAPI(flask_app, version='1.0.0', title='OMRService API', +api = CustomAPI(flask_app, version='1.0.0', title='DashboardsService API', description='DasiboardsService API Documentation', doc='/doc', prefix='/api', authorizations=authorizations) @@ -172,7 +172,6 @@ def __init__(self, config: ConfigManager, service): flask_app.config.update({'BABEL_DEFAULT_LOCALE': 'fr'}) flask_app.config.update({'SESSION_COOKIE_SECURE': True}) - # Init API self.init_service_api(self, self.service, service_api_ns) self.init_user_api(self, self.service, user_api_ns) @@ -228,14 +227,14 @@ def init_service_api(module: object, service: object, api_ns=service_api_ns, add # Default arguments kwargs = {'flaskModule': module, 'service': service} | additional_args - @staticmethod def init_user_api(module: object, service: object, api_ns=user_api_ns, additional_args={}): # Default arguments kwargs = {'flaskModule': module, 'service': service} | additional_args - pass + from API.user.QueryDashboard import QueryDashboard + api_ns.add_resource(QueryDashboard, '/dashboards', resource_class_kwargs=kwargs) @staticmethod def init_participant_api(module: object, service: object, api_ns=participant_api_ns, additional_args={}): @@ -244,8 +243,6 @@ def init_participant_api(module: object, service: object, api_ns=participant_api pass - - def init_views(self): # Default arguments args = [] diff --git a/libDashboards/db/DBManager.py b/libDashboards/db/DBManager.py index b528f3f..bdd18d3 100644 --- a/libDashboards/db/DBManager.py +++ b/libDashboards/db/DBManager.py @@ -31,8 +31,12 @@ def __init__(self, app=flask_app, test: bool = False): self.app = app self.test = test - def create_defaults(self, config: ConfigManager, test=False): - pass + def create_defaults(self, test=False): + with self.app.app_context(): + from libDashboards.db.models.DashDashboards import DashDashboards + if DashDashboards.get_count() == 0: + print("No dashboards - creating defaults") + DashDashboards.create_defaults(test) def open(self, db_infos, echo=False): self.db_uri = 'postgresql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % db_infos @@ -46,7 +50,6 @@ def open(self, db_infos, echo=False): # Create db engine self.db.init_app(self.app) self.db.app = self.app - BaseModel.set_db(self.db) with self.app.app_context(): diff --git a/libDashboards/db/models/DashDashboards.py b/libDashboards/db/models/DashDashboards.py new file mode 100644 index 0000000..4091b2b --- /dev/null +++ b/libDashboards/db/models/DashDashboards.py @@ -0,0 +1,102 @@ +from libDashboards.db.models.BaseModel import BaseModel +from sqlalchemy import Column, Integer, Sequence, String, Boolean +import uuid + + +class DashDashboards(BaseModel): + __tablename__ = 't_dashboards' + id_dashboard = Column(Integer, Sequence('id_dashboard_sequence'), primary_key=True, autoincrement=True) + id_site = Column(Integer, nullable=True) + id_project = Column(Integer, nullable=True) + + dashboard_uuid = Column(String(36), nullable=False) + dashboard_name = Column(String, nullable=False) + dashboard_enabled = Column(Boolean, nullable=False, default=True) + dashboard_description = Column(String, nullable=True) # Dashboard user-visible description + dashboard_definition = Column(String, nullable=False) # Dashboard definition string + dashboard_version = Column(Integer, nullable=False, default=1) + + def to_json(self, ignore_fields=None, minimal=False): + if ignore_fields is None: + ignore_fields = [] + + if minimal: + ignore_fields.extend(['asset_definition']) + + asset_json = super().to_json(ignore_fields=ignore_fields) + + return asset_json + + @staticmethod + def get_dashboard_by_uuid(dashboard_uuid: str, latest=True): + query = DashDashboards.query.filter_by(dashboard_uuid=dashboard_uuid) + if latest: + query = query.order_by(DashDashboards.dashboard_version).desc() + + return query.first() + + @staticmethod + def get_dashboards_for_site(site_id: int, latest=True) -> []: + return DashDashboards.query.filter_by(id_site=site_id).all() + + @staticmethod + def get_dashboards_for_project(project_id: int, latest=True) -> []: + query = DashDashboards.query.filter_by(id_project=project_id) + # if latest: + # query = query.group_by(DashDashboards.dashboard_uuid) + return query.all() + + @staticmethod + def get_dashboards_globals(latest=True) -> []: + return DashDashboards.query.filter_by(id_project=None, id_site=None).all() + + @classmethod + def insert(cls, dashboard): + # Generate UUID + if not dashboard.dashboard_uuid: + dashboard.dashboard_uuid = str(uuid.uuid4()) + + super().insert(dashboard) + + @staticmethod + def create_defaults(test=False): + if test: + # Create dashboard for site... + dashboard = DashDashboards() + dashboard.id_site = 1 + dashboard.dashboard_name = 'Site 1 - Global' + dashboard.dashboard_description = 'Test dashboard for global site overview' + dashboard.dashboard_definition = '{}' + DashDashboards.insert(dashboard) + + # ... for project... + dashboard = DashDashboards() + dashboard.id_project = 1 + dashboard.dashboard_name = 'Project 1 - Global' + dashboard.dashboard_description = 'Test dashboard for global project overview' + dashboard.dashboard_definition = '{}' + DashDashboards.insert(dashboard) + + dashboard = DashDashboards() + dashboard.id_project = 1 + dashboard.dashboard_name = 'Project 1 - Alerts' + dashboard.dashboard_description = 'Test dashboard for project alerts' + dashboard.dashboard_definition = '{}' + DashDashboards.insert(dashboard) + uuid = dashboard.dashboard_uuid + + dashboard = DashDashboards() + dashboard.id_project = 1 + dashboard.dashboard_name = 'Project 1 - Alerts v2' + dashboard.dashboard_description = 'Test dashboard for project alerts' + dashboard.dashboard_definition = '{}' + dashboard.dashboard_version = 2 + dashboard.dashboard_uuid = uuid + DashDashboards.insert(dashboard) + + # ... and globals + dashboard = DashDashboards() + dashboard.dashboard_name = 'System Dashboard' + dashboard.dashboard_description = 'Global system dashboard' + dashboard.dashboard_definition = '{}' + DashDashboards.insert(dashboard) diff --git a/libDashboards/db/models/__init__.py b/libDashboards/db/models/__init__.py new file mode 100644 index 0000000..4ae081d --- /dev/null +++ b/libDashboards/db/models/__init__.py @@ -0,0 +1,6 @@ +from .DashDashboards import DashDashboards + +# All exported symbols +__all__ = [ + 'DashDashboards' +]