]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add token authentication to Grafana proxy
authorPatrick Nawracay <pnawracay@suse.com>
Fri, 18 May 2018 07:38:20 +0000 (09:38 +0200)
committerPatrick Nawracay <pnawracay@suse.com>
Thu, 14 Jun 2018 11:54:00 +0000 (13:54 +0200)
Enables token authentication for the Grafana proxy as additional option
to username/password authentication. The authentication method has to be
set, too.

    $ ceph dashboard set-grafana-api-token <token>    # default: ''
    $ ceph dashboard set-grafana-api-auth-method <method>  # default: ''

Possible values for the authentication method are 'password' and
'token'.

Signed-off-by: Patrick Nawracay <pnawracay@suse.com>
doc/mgr/dashboard.rst
src/pybind/mgr/dashboard/controllers/grafana.py
src/pybind/mgr/dashboard/settings.py
src/pybind/mgr/dashboard/tests/test_grafana.py

index 94d6ca4e4b412764e6353f7cda48100cf0108fb0..ec3f0f41764d0fb677cf81343232f8f5239e6013 100644 (file)
@@ -278,13 +278,31 @@ Grafana on your preferred hosts, proceed with the following steps::
 
         root_url = http://localhost:3000/api/grafana/proxy
 
-After you have configured Grafana and Prometheus, you will need to tell the
-Ceph Manager Dashboard where it can access Grafana and what the credentials are
-to do so. This can be done by using the following commands::
+After you have set up Grafana and Prometheus, you will need to configure the
+connection information that the Ceph Manager Dashboard will use to access Grafana.
+This includes setting the authentication method to be used, the corresponding login
+credentials as well as the URL at which the Grafana instance can be reached.
+
+The URL and TCP port can be set by using the following command::
 
   $ ceph dashboard set-grafana-api-url <url>  # default: 'http://localhost:3000'
-  $ ceph dashboard set-grafana-api-username <username> # default: 'admin'
-  $ ceph dashboard set-grafana-api-password <password> # default: 'admin'
+
+You need to tell the dashboard which authentication method should be
+used::
+
+  $ ceph dashboard set-grafana-api-auth-method <method>  # default: ''
+
+Possible values are either 'password' or 'token'.
+
+To authenticate via username and password, you will need to set the following
+values::
+
+  $ ceph dashboard set-grafana-api-username <username>  # default: 'admin'
+  $ ceph dashboard set-grafana-api-password <password>  # default: 'admin'
+
+To use token based authentication, you will ned to set the token by issuing::
+
+  $ ceph dashboard set-grafana-api-token <token>  # default: ''
 
 Accessing the dashboard
 ^^^^^^^^^^^^^^^^^^^^^^^
index 37f2568d1d27b4bfbb5db3a9228204336f8d2c2b..cffb6d0b976814167b799a723d5a2428a535f68b 100644 (file)
@@ -13,29 +13,32 @@ from ..settings import Settings
 class GrafanaRestClient(object):
     _instance = None
 
-    @staticmethod
-    def _raise_for_validation(url, user, password):
+    def _raise_for_validation(self):
         msg = 'No {} found or misconfigured, please consult the ' \
               'documentation about how to configure Grafana for the dashboard.'
 
-        o = urlparse(url)
+        o = urlparse(self._url)
         if not (o.netloc and o.scheme):
             raise LookupError(msg.format('URL'))
 
-        if not all((user, password)):
-            raise LookupError(msg.format('credentials'))
+        auth_method = 'password' if self._token is None else 'token'
+        if auth_method == 'password' and not all((self._user, self._password)):
+            raise LookupError(msg.format('username and/or password'))
+        elif auth_method == 'token' and not self._token:
+            raise LookupError(msg.format('token'))
 
-    def __init__(self, url, username, password):
+    def __init__(self, url, username=None, password=None, token=None):
         """
         :type url: str
         :type username: str
         :type password: str
         """
-        self._raise_for_validation(url, username, password)
-
         self._url = url.rstrip('/')
         self._user = username
         self._password = password
+        self._token = token
+
+        self._raise_for_validation()
 
     @classmethod
     def instance(cls):
@@ -46,11 +49,19 @@ class GrafanaRestClient(object):
         :rtype: GrafanaRestClient
         """
         if not cls._instance:
-            url = Settings.GRAFANA_API_URL
-            user = Settings.GRAFANA_API_USERNAME
-            password = Settings.GRAFANA_API_PASSWORD
-
-            cls._instance = GrafanaRestClient(url, user, password)
+            kwargs = {}
+            if Settings.GRAFANA_API_AUTH_METHOD.lower() == 'password':
+                kwargs['username'] = Settings.GRAFANA_API_USERNAME
+                kwargs['password'] = Settings.GRAFANA_API_PASSWORD
+            elif Settings.GRAFANA_API_AUTH_METHOD.lower() == 'token':
+                kwargs['token'] = Settings.GRAFANA_API_TOKEN
+            else:
+                raise LookupError('No or unknown authentication method '
+                                  'provided. Please consult the documentation '
+                                  'about how to configure the '
+                                  'Grafana integration correctly.')
+            cls._instance = GrafanaRestClient(Settings.GRAFANA_API_URL,
+                                              **kwargs)
 
         return cls._instance
 
@@ -61,13 +72,19 @@ class GrafanaRestClient(object):
         headers = {k: v for k, v in cherrypy.request.headers.items()
                    if k.lower() in ('content-type', 'accept')}
 
+        auth = None
+        if self._token:
+            headers['Authorization'] = 'Bearer {}'.format(self._token)
+        else:
+            auth = (self._user, self._password)
+
         response = requests.request(
             method,
             url,
             params=params,
             data=data,
             headers=headers,
-            auth=(self._user, self._password))
+            auth=auth)
         logger.debug("proxying method=%s path=%s params=%s data=%s", method,
                      path, params, data)
 
index aaa8260bb260904105296f9f27231eed384a7af9..fb1e1370466c84757a68789473c439145b14b342 100644 (file)
@@ -33,6 +33,8 @@ class Options(object):
     GRAFANA_API_URL = ('http://localhost:3000', str)
     GRAFANA_API_USERNAME = ('admin', str)
     GRAFANA_API_PASSWORD = ('admin', str)
+    GRAFANA_API_TOKEN = ('', str)
+    GRAFANA_API_AUTH_METHOD = ('', str)  # Either 'password' or 'token'
 
     @staticmethod
     def has_default_value(name):
index 6f3a08f6840a9f4adfeec1478d1610c7805c7104..975fcb62d4bc62863c2d54afed5c2e200da18035 100644 (file)
@@ -11,16 +11,21 @@ from .helper import ControllerTestCase
 
 class Grafana(TestCase):
     def test_missing_credentials(self):
-        with six.assertRaisesRegex(self, LookupError, r'^No credentials.*'):
+        with six.assertRaisesRegex(self, LookupError,
+                                   r'username and/or password'):
             GrafanaRestClient(
                 url='http://localhost:3000', username='', password='admin')
-
+        with six.assertRaisesRegex(self, LookupError, r'token'):
+            GrafanaRestClient(
+                url='http://localhost:3000',
+                token='',
+            )
         with six.assertRaisesRegex(self, LookupError, r'^No URL.*'):
             GrafanaRestClient(
                 url='//localhost:3000', username='admin', password='admin')
 
 
-@Controller('/grafana/mocked')
+@Controller('grafana/mocked')
 class GrafanaMockInstance(BaseController):
     @Proxy()
     def __call__(self, path, **params):
@@ -32,10 +37,10 @@ class GrafanaControllerTestCase(ControllerTestCase):
     @classmethod
     def setup_server(cls):
         settings = {
-            'GRAFANA_API_URL':
-                'http://localhost:{}/grafana/mocked/'.format(54583),
+            'GRAFANA_API_URL': 'http://localhost:{}/grafana/mocked/'.format(54583),
             'GRAFANA_API_USERNAME': 'admin',
             'GRAFANA_API_PASSWORD': 'admin',
+            'GRAFANA_API_AUTH_METHOD': 'password',
         }
         mgr.get_config.side_effect = settings.get
         GrafanaProxy._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access