]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: automate cors config
authorNizamudeen A <nia@redhat.com>
Tue, 9 Jan 2024 10:39:15 +0000 (16:09 +0530)
committerNizamudeen A <nia@redhat.com>
Tue, 13 Feb 2024 09:59:38 +0000 (15:29 +0530)
Signed-off-by: Nizamudeen A <nia@redhat.com>
src/pybind/mgr/dashboard/controllers/multi_cluster.py
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/tools.py

index 652fafd25a3c4c10d0d7eea313f09caa17b036f0..cfb99d201d1b428e185914258d13dbbef139a740 100644 (file)
@@ -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)
index 41160b698aae67c0653fee05dcb89b50c625c440..83784197656476ad449948eb2ee96512af299ce6 100644 (file)
@@ -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']
index 1fbaac399aaa9cfc26f77ae2a976a07c7c090f6f..59f1fea951d424556e4e32c75284775cd4fb35c2 100644 (file)
@@ -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
index 4e4837d9323e04b5a28c0be58de00353268cb42b..5619b5bb90d57bb76b65d4794c446942c7159049 100644 (file)
@@ -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