From 51f3a011d90d8ba530c2e25ab2161e3fb13e8293 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Tue, 9 Jan 2024 16:09:15 +0530 Subject: [PATCH] mgr/dashboard: automate cors config Signed-off-by: Nizamudeen A --- .../dashboard/controllers/multi_cluster.py | 57 ++++++++++----- src/pybind/mgr/dashboard/module.py | 68 +----------------- src/pybind/mgr/dashboard/openapi.yaml | 51 ++++++++++++++ src/pybind/mgr/dashboard/tools.py | 69 +++++++++++++++++++ 4 files changed, 161 insertions(+), 84 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/multi_cluster.py b/src/pybind/mgr/dashboard/controllers/multi_cluster.py index 652fafd25a3c4..cfb99d201d1b4 100644 --- a/src/pybind/mgr/dashboard/controllers/multi_cluster.py +++ b/src/pybind/mgr/dashboard/controllers/multi_cluster.py @@ -1,28 +1,37 @@ # -*- coding: utf-8 -*- import json + import requests -from . import APIDoc, APIRouter, CreatePermission, UpdatePermission, ReadPermission, Endpoint, EndpointDoc, RESTController from ..exceptions import DashboardException -from ..settings import Settings from ..security import Scope +from ..settings import Settings +from ..tools import configure_cors +from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \ + RESTController, UIRouter, UpdatePermission @APIRouter('/multi-cluster', Scope.CONFIG_OPT) @APIDoc('Multi-cluster Management API', 'Multi-cluster') class MultiCluster(RESTController): - def _proxy(self, method, base_url, path, params=None, payload=None, verify=False): + def _proxy(self, method, base_url, path, params=None, payload=None, verify=False, token=None): try: - headers = { - 'Accept': 'application/vnd.ceph.api.v1.0+json', - 'Content-Type': 'application/json', - } + if token: + headers = { + 'Accept': 'application/vnd.ceph.api.v1.0+json', + 'Authorization': 'Bearer ' + token, + } + else: + headers = { + 'Accept': 'application/vnd.ceph.api.v1.0+json', + 'Content-Type': 'application/json', + } response = requests.request(method, base_url + path, params=params, json=payload, verify=verify, headers=headers) except Exception as e: raise DashboardException( - "Could not reach {}".format(base_url+path), + "Could not reach {}, {}".format(base_url+path, e), http_status_code=404, component='dashboard') @@ -34,11 +43,10 @@ class MultiCluster(RESTController): component='dashboard') return content - @Endpoint('POST') @CreatePermission @EndpointDoc("Authenticate to a remote cluster") - def auth(self, url: str, name: str, username=None, password=None, token=None): + def auth(self, url: str, name: str, username=None, password=None, token=None, hub_url=None): multicluster_config = {} if isinstance(Settings.MULTICLUSTER_CONFIG, str): @@ -50,6 +58,9 @@ class MultiCluster(RESTController): else: multicluster_config = Settings.MULTICLUSTER_CONFIG.copy() + if 'hub_url' not in multicluster_config: + multicluster_config['hub_url'] = hub_url + if 'config' not in multicluster_config: multicluster_config['config'] = [] @@ -75,13 +86,23 @@ class MultiCluster(RESTController): http_status_code=400, component='dashboard') - else: - token = content['token'] + token = content['token'] + # Set CORS endpoint on remote cluster + self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint', + payload={'url': multicluster_config['hub_url']}, token=token) + + multicluster_config['config'].append({ + 'name': name, + 'url': url, + 'token': token + }) + + Settings.MULTICLUSTER_CONFIG = multicluster_config - multicluster_config['config'].append({ - 'name': name, - 'url': url, - 'token': token - }) - Settings.MULTICLUSTER_CONFIG = multicluster_config +@UIRouter('/multi-cluster', Scope.CONFIG_OPT) +class MultiClusterUi(RESTController): + @Endpoint('PUT') + @UpdatePermission + def set_cors_endpoint(self, url: str): + configure_cors(url) diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index 41160b698aae6..8378419765647 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -36,7 +36,7 @@ from .services.rgw_client import configure_rgw_credentials from .services.sso import SSO_COMMANDS, handle_sso_command from .settings import handle_option_command, options_command_list, options_schema_list from .tools import NotificationQueue, RequestLoggingTool, TaskManager, \ - prepare_url_prefix, str_to_bool + configure_cors, prepare_url_prefix, str_to_bool try: import cherrypy @@ -120,7 +120,7 @@ class CherryPyConfig(object): # Initialize custom handlers. cherrypy.tools.authenticate = AuthManagerTool() - self.configure_cors() + configure_cors() cherrypy.tools.plugin_hooks_filter_request = cherrypy.Tool( 'before_handler', lambda: PLUGIN_MANAGER.hook.filter_request_before_handler(request=cherrypy.request), @@ -223,70 +223,6 @@ class CherryPyConfig(object): self.log.info("Configured CherryPy, starting engine...") # type: ignore return uri - def configure_cors(self): - """ - Allow CORS requests if the cross_origin_url option is set. - """ - cross_origin_url = mgr.get_localized_module_option('cross_origin_url', '') - if cross_origin_url: - cherrypy.tools.CORS = cherrypy.Tool('before_handler', self.cors_tool) - config = { - 'tools.CORS.on': True, - } - self.update_cherrypy_config(config) - - def cors_tool(self): - ''' - Handle both simple and complex CORS requests - - Add CORS headers to each response. If the request is a CORS preflight - request swap out the default handler with a simple, single-purpose handler - that verifies the request and provides a valid CORS response. - ''' - req_head = cherrypy.request.headers - resp_head = cherrypy.response.headers - - # Always set response headers necessary for 'simple' CORS. - req_header_cross_origin_url = req_head.get('Access-Control-Allow-Origin') - cross_origin_urls = mgr.get_localized_module_option('cross_origin_url', '') - cross_origin_url_list = [url.strip() for url in cross_origin_urls.split(',')] - if req_header_cross_origin_url in cross_origin_url_list: - resp_head['Access-Control-Allow-Origin'] = req_header_cross_origin_url - resp_head['Access-Control-Expose-Headers'] = 'GET, POST' - resp_head['Access-Control-Allow-Credentials'] = 'true' - - # Non-simple CORS preflight request; short-circuit the normal handler. - if cherrypy.request.method == 'OPTIONS': - req_header_origin_url = req_head.get('Origin') - if req_header_origin_url in cross_origin_url_list: - resp_head['Access-Control-Allow-Origin'] = req_header_origin_url - ac_method = req_head.get('Access-Control-Request-Method', None) - - allowed_methods = ['GET', 'POST', 'PUT'] - allowed_headers = [ - 'Content-Type', - 'Authorization', - 'Accept', - 'Access-Control-Allow-Origin' - ] - - if ac_method and ac_method in allowed_methods: - resp_head['Access-Control-Allow-Methods'] = ', '.join(allowed_methods) - resp_head['Access-Control-Allow-Headers'] = ', '.join(allowed_headers) - - resp_head['Connection'] = 'keep-alive' - resp_head['Access-Control-Max-Age'] = '3600' - - # CORS requests should short-circuit the other tools. - cherrypy.response.body = ''.encode('utf8') - cherrypy.response.status = 200 - cherrypy.serving.request.handler = None - - # Needed to avoid the auth_tool check. - if cherrypy.request.config.get('tools.sessions.on', False): - cherrypy.session['token'] = True - return True - if TYPE_CHECKING: SslConfigKey = Literal['crt', 'key'] diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 1fbaac399aaa9..59f1fea951d42 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -6932,6 +6932,55 @@ paths: summary: Get Monitor Details tags: - Monitor + /api/multi-cluster/auth: + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + hub_url: + type: string + name: + type: string + password: + type: string + token: + type: string + url: + type: string + username: + type: string + required: + - url + - name + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Authenticate to a remote cluster + tags: + - Multi-cluster /api/nfs-ganesha/cluster: get: parameters: [] @@ -14019,6 +14068,8 @@ tags: name: MonPerfCounter - description: Get Monitor Details name: Monitor +- description: Multi-cluster Management API + name: Multi-cluster - description: NFS-Ganesha Cluster Management API name: NFS-Ganesha - description: NVMe-oF Gateway Management API diff --git a/src/pybind/mgr/dashboard/tools.py b/src/pybind/mgr/dashboard/tools.py index 4e4837d9323e0..5619b5bb90d57 100644 --- a/src/pybind/mgr/dashboard/tools.py +++ b/src/pybind/mgr/dashboard/tools.py @@ -838,3 +838,72 @@ def merge_list_of_dicts_by_key(target_list: list, source_list: list, key: str): target_list[sdict[key]].update(sdict) target_list = [value for value in target_list.values()] return target_list + + +def configure_cors(url: str = ''): + """ + Allow CORS requests if the cross_origin_url option is set. + """ + if url: + cross_origin_url = url + mgr.set_module_option('cross_origin_url', cross_origin_url) + else: + cross_origin_url = mgr.get_localized_module_option('cross_origin_url', '') + if cross_origin_url: + cherrypy.tools.CORS = cherrypy.Tool('before_handler', cors_tool) + config = { + 'tools.CORS.on': True, + } + cherrypy.config.update(config) + + +def cors_tool(): + ''' + Handle both simple and complex CORS requests + Add CORS headers to each response. If the request is a CORS preflight + request swap out the default handler with a simple, single-purpose handler + that verifies the request and provides a valid CORS response. + ''' + req_head = cherrypy.request.headers + resp_head = cherrypy.response.headers + + # Always set response headers necessary for 'simple' CORS. + req_header_cross_origin_url = req_head.get('Access-Control-Allow-Origin') + cross_origin_urls = mgr.get_localized_module_option('cross_origin_url', '') + cross_origin_url_list = [url.strip() for url in cross_origin_urls.split(',')] + if req_header_cross_origin_url in cross_origin_url_list: + resp_head['Access-Control-Allow-Origin'] = req_header_cross_origin_url + resp_head['Access-Control-Expose-Headers'] = 'GET, POST' + resp_head['Access-Control-Allow-Credentials'] = 'true' + + # Non-simple CORS preflight request; short-circuit the normal handler. + if cherrypy.request.method == 'OPTIONS': + req_header_origin_url = req_head.get('Origin') + if req_header_origin_url in cross_origin_url_list: + resp_head['Access-Control-Allow-Origin'] = req_header_origin_url + ac_method = req_head.get('Access-Control-Request-Method', None) + + allowed_methods = ['GET', 'POST', 'PUT'] + allowed_headers = [ + 'Content-Type', + 'Authorization', + 'Accept', + 'Access-Control-Allow-Origin' + ] + + if ac_method and ac_method in allowed_methods: + resp_head['Access-Control-Allow-Methods'] = ', '.join(allowed_methods) + resp_head['Access-Control-Allow-Headers'] = ', '.join(allowed_headers) + + resp_head['Connection'] = 'keep-alive' + resp_head['Access-Control-Max-Age'] = '3600' + + # CORS requests should short-circuit the other tools. + cherrypy.response.body = ''.encode('utf8') + cherrypy.response.status = 200 + cherrypy.serving.request.handler = None + + # Needed to avoid the auth_tool check. + if cherrypy.request.config.get('tools.sessions.on', False): + cherrypy.session['token'] = True + return True -- 2.39.5