From bd2196b6ca4c03150d323fbd205abfc63a7473fb Mon Sep 17 00:00:00 2001 From: Patrick Nawracay Date: Fri, 18 May 2018 09:38:20 +0200 Subject: [PATCH] mgr/dashboard: Add token authentication to Grafana proxy 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 # default: '' $ ceph dashboard set-grafana-api-auth-method # default: '' Possible values for the authentication method are 'password' and 'token'. Signed-off-by: Patrick Nawracay --- doc/mgr/dashboard.rst | 28 +++++++++--- .../mgr/dashboard/controllers/grafana.py | 45 +++++++++++++------ src/pybind/mgr/dashboard/settings.py | 2 + .../mgr/dashboard/tests/test_grafana.py | 15 ++++--- 4 files changed, 66 insertions(+), 24 deletions(-) diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 94d6ca4e4b4..ec3f0f41764 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -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 # default: 'http://localhost:3000' - $ ceph dashboard set-grafana-api-username # default: 'admin' - $ ceph dashboard set-grafana-api-password # default: 'admin' + +You need to tell the dashboard which authentication method should be +used:: + + $ ceph dashboard set-grafana-api-auth-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 # default: 'admin' + $ ceph dashboard set-grafana-api-password # default: 'admin' + +To use token based authentication, you will ned to set the token by issuing:: + + $ ceph dashboard set-grafana-api-token # default: '' Accessing the dashboard ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/pybind/mgr/dashboard/controllers/grafana.py b/src/pybind/mgr/dashboard/controllers/grafana.py index 37f2568d1d2..cffb6d0b976 100644 --- a/src/pybind/mgr/dashboard/controllers/grafana.py +++ b/src/pybind/mgr/dashboard/controllers/grafana.py @@ -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) diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py index aaa8260bb26..fb1e1370466 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -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): diff --git a/src/pybind/mgr/dashboard/tests/test_grafana.py b/src/pybind/mgr/dashboard/tests/test_grafana.py index 6f3a08f6840..975fcb62d4b 100644 --- a/src/pybind/mgr/dashboard/tests/test_grafana.py +++ b/src/pybind/mgr/dashboard/tests/test_grafana.py @@ -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 -- 2.39.5