]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Use secure cookies to store JWT Token 38839/head
authorAashish Sharma <aashishsharma@localhost.localdomain>
Tue, 24 Nov 2020 05:58:28 +0000 (11:28 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Thu, 28 Jan 2021 14:15:05 +0000 (19:45 +0530)
This PR intends to store the jwt token in secure cookies instead of local storage

Fixes: https://tracker.ceph.com/issues/44591
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
(cherry picked from commit 36703c63381e6723fff57266235f8230e6af1d92)
(cherry picked from commit 3c72dc309936b23e413dc1aee8ca49c795c48a0f)
(cherry picked from commit ea9876d6dfc7033a61cac68c985fd55fd52f6064)
(cherry picked from commit d3218fcb44d9202d751380f6b2702b14cf420e1f)
(cherry picked from commit 2cd7ce9e69b6a14778432abd17b34ba9e4020abc)

20 files changed:
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_auth.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/docs.py
src/pybind/mgr/dashboard/controllers/saml2.py
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/app.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts
src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh
src/pybind/mgr/dashboard/services/auth.py

index d7e6accee0b8162af4d3f0f1e235f78fbb744516..01f2d3ff8d8db480bb7da4ecc877291db3add48e 100644 (file)
@@ -78,17 +78,20 @@ class DashboardTestCase(MgrTestCase):
         cls._ceph_cmd(set_roles_args)
 
     @classmethod
-    def login(cls, username, password):
+    def login(cls, username, password, set_cookies=False):
         if cls._loggedin:
             cls.logout()
-        cls._post('/api/auth', {'username': username, 'password': password})
+        cls._post('/api/auth', {'username': username,
+                                'password': password}, set_cookies=set_cookies)
+        cls._assertEq(cls._resp.status_code, 201)
         cls._token = cls.jsonBody()['token']
         cls._loggedin = True
 
     @classmethod
-    def logout(cls):
+    def logout(cls, set_cookies=False):
         if cls._loggedin:
-            cls._post('/api/auth/logout')
+            cls._post('/api/auth/logout', set_cookies=set_cookies)
+            cls._assertEq(cls._resp.status_code, 200)
             cls._token = None
             cls._loggedin = False
 
@@ -169,29 +172,49 @@ class DashboardTestCase(MgrTestCase):
     def tearDownClass(cls):
         super(DashboardTestCase, cls).tearDownClass()
 
-    # pylint: disable=inconsistent-return-statements
+    # pylint: disable=inconsistent-return-statements, too-many-arguments, too-many-branches
     @classmethod
-    def _request(cls, url, method, data=None, params=None):
+    def _request(cls, url, method, data=None, params=None, set_cookies=False):
         url = "{}{}".format(cls._base_uri, url)
         log.info("Request %s to %s", method, url)
         headers = {}
+        cookies = {}
         if cls._token:
-            headers['Authorization'] = "Bearer {}".format(cls._token)
-
-        if method == 'GET':
-            cls._resp = cls._session.get(url, params=params, verify=False,
-                                         headers=headers)
-        elif method == 'POST':
-            cls._resp = cls._session.post(url, json=data, params=params,
-                                          verify=False, headers=headers)
-        elif method == 'DELETE':
-            cls._resp = cls._session.delete(url, json=data, params=params,
-                                            verify=False, headers=headers)
-        elif method == 'PUT':
-            cls._resp = cls._session.put(url, json=data, params=params,
-                                         verify=False, headers=headers)
+            if set_cookies:
+                cookies['token'] = cls._token
+            else:
+                headers['Authorization'] = "Bearer {}".format(cls._token)
+
+        if set_cookies:
+            if method == 'GET':
+                cls._resp = cls._session.get(url, params=params, verify=False,
+                                             headers=headers, cookies=cookies)
+            elif method == 'POST':
+                cls._resp = cls._session.post(url, json=data, params=params,
+                                              verify=False, headers=headers, cookies=cookies)
+            elif method == 'DELETE':
+                cls._resp = cls._session.delete(url, json=data, params=params,
+                                                verify=False, headers=headers, cookies=cookies)
+            elif method == 'PUT':
+                cls._resp = cls._session.put(url, json=data, params=params,
+                                             verify=False, headers=headers, cookies=cookies)
+            else:
+                assert False
         else:
-            assert False
+            if method == 'GET':
+                cls._resp = cls._session.get(url, params=params, verify=False,
+                                             headers=headers)
+            elif method == 'POST':
+                cls._resp = cls._session.post(url, json=data, params=params,
+                                              verify=False, headers=headers)
+            elif method == 'DELETE':
+                cls._resp = cls._session.delete(url, json=data, params=params,
+                                                verify=False, headers=headers)
+            elif method == 'PUT':
+                cls._resp = cls._session.put(url, json=data, params=params,
+                                             verify=False, headers=headers)
+            else:
+                assert False
         try:
             if not cls._resp.ok:
                 # Output response for easier debugging.
@@ -205,8 +228,8 @@ class DashboardTestCase(MgrTestCase):
             raise ex
 
     @classmethod
-    def _get(cls, url, params=None):
-        return cls._request(url, 'GET', params=params)
+    def _get(cls, url, params=None, set_cookies=False):
+        return cls._request(url, 'GET', params=params, set_cookies=set_cookies)
 
     @classmethod
     def _view_cache_get(cls, url, retries=5):
@@ -227,16 +250,16 @@ class DashboardTestCase(MgrTestCase):
         return res
 
     @classmethod
-    def _post(cls, url, data=None, params=None):
-        cls._request(url, 'POST', data, params)
+    def _post(cls, url, data=None, params=None, set_cookies=False):
+        cls._request(url, 'POST', data, params, set_cookies=set_cookies)
 
     @classmethod
-    def _delete(cls, url, data=None, params=None):
-        cls._request(url, 'DELETE', data, params)
+    def _delete(cls, url, data=None, params=None, set_cookies=False):
+        cls._request(url, 'DELETE', data, params, set_cookies=set_cookies)
 
     @classmethod
-    def _put(cls, url, data=None, params=None):
-        cls._request(url, 'PUT', data, params)
+    def _put(cls, url, data=None, params=None, set_cookies=False):
+        cls._request(url, 'PUT', data, params, set_cookies=set_cookies)
 
     @classmethod
     def _assertEq(cls, v1, v2):
@@ -255,9 +278,9 @@ class DashboardTestCase(MgrTestCase):
 
     # pylint: disable=too-many-arguments
     @classmethod
-    def _task_request(cls, method, url, data, timeout):
-        res = cls._request(url, method, data)
-        cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403])
+    def _task_request(cls, method, url, data, timeout, set_cookies=False):
+        res = cls._request(url, method, data, set_cookies=set_cookies)
+        cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403, 404])
 
         if cls._resp.status_code == 403:
             return None
@@ -308,16 +331,16 @@ class DashboardTestCase(MgrTestCase):
             return res_task['exception']
 
     @classmethod
-    def _task_post(cls, url, data=None, timeout=60):
-        return cls._task_request('POST', url, data, timeout)
+    def _task_post(cls, url, data=None, timeout=60, set_cookies=False):
+        return cls._task_request('POST', url, data, timeout, set_cookies=set_cookies)
 
     @classmethod
-    def _task_delete(cls, url, timeout=60):
-        return cls._task_request('DELETE', url, None, timeout)
+    def _task_delete(cls, url, timeout=60, set_cookies=False):
+        return cls._task_request('DELETE', url, None, timeout, set_cookies=set_cookies)
 
     @classmethod
-    def _task_put(cls, url, data=None, timeout=60):
-        return cls._task_request('PUT', url, data, timeout)
+    def _task_put(cls, url, data=None, timeout=60, set_cookies=False):
+        return cls._task_request('PUT', url, data, timeout, set_cookies=set_cookies)
 
     @classmethod
     def cookies(cls):
index 87bdd8771bd977a3d64186268bfa4fd603b54034..f3df04c0a290596dcf058d66f37128d233aad199 100644 (file)
@@ -36,6 +36,7 @@ class AuthTest(DashboardTestCase):
             self.create_user('admin2', '', ['administrator'])
 
     def test_a_set_login_credentials(self):
+        # test with Authorization header
         self.create_user('admin2', 'admin2', ['administrator'])
         self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
         self.assertStatus(201)
@@ -43,13 +44,29 @@ class AuthTest(DashboardTestCase):
         self._validate_jwt_token(data['token'], "admin2", data['permissions'])
         self.delete_user('admin2')
 
+        # test with Cookies set
+        self.create_user('admin2', 'admin2', ['administrator'])
+        self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin2", data['permissions'])
+        self.delete_user('admin2')
+
     def test_login_valid(self):
+        # test with Authorization header
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
         data = self.jsonBody()
         self._validate_jwt_token(data['token'], "admin", data['permissions'])
 
+        # test with Cookies set
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
+
     def test_login_invalid(self):
+        # test with Authorization header
         self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
         self.assertStatus(400)
         self.assertJsonBody({
@@ -58,7 +75,17 @@ class AuthTest(DashboardTestCase):
             "detail": "Invalid credentials"
         })
 
+        # test with Cookies set
+        self._post("/api/auth", {'username': 'admin', 'password': 'inval'}, set_cookies=True)
+        self.assertStatus(400)
+        self.assertJsonBody({
+            "component": "auth",
+            "code": "invalid_credentials",
+            "detail": "Invalid credentials"
+        })
+
     def test_logout(self):
+        # test with Authorization header
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
         data = self.jsonBody()
@@ -73,7 +100,23 @@ class AuthTest(DashboardTestCase):
         self.assertStatus(401)
         self.set_jwt_token(None)
 
+        # test with Cookies set
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
+        self.set_jwt_token(data['token'])
+        self._post("/api/auth/logout", set_cookies=True)
+        self.assertStatus(200)
+        self.assertJsonBody({
+            "redirect_url": "#/login"
+        })
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self.set_jwt_token(None)
+
     def test_token_ttl(self):
+        # test with Authorization header
         self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
@@ -86,7 +129,21 @@ class AuthTest(DashboardTestCase):
         self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
         self.set_jwt_token(None)
 
+        # test with Cookies set
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(200)
+        time.sleep(6)
+        self._get("/api/host")
+        self.assertStatus(401)
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
+        self.set_jwt_token(None)
+
     def test_remove_from_blacklist(self):
+        # test with Authorization header
         self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
@@ -106,11 +163,37 @@ class AuthTest(DashboardTestCase):
         self._post("/api/auth/logout")
         self.assertStatus(200)
 
+        # test with Cookies set
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        # the following call adds the token to the blocklist
+        self._post("/api/auth/logout", set_cookies=True)
+        self.assertStatus(200)
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        time.sleep(6)
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
+        self.set_jwt_token(None)
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        # the following call removes expired tokens from the blocklist
+        self._post("/api/auth/logout", set_cookies=True)
+        self.assertStatus(200)
+
     def test_unauthorized(self):
+        # test with Authorization header
         self._get("/api/host")
         self.assertStatus(401)
 
+        # test with Cookies set
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+
     def test_invalidate_token_by_admin(self):
+        # test with Authorization header
         self._get("/api/host")
         self.assertStatus(401)
         self.create_user('user', 'user', ['read-only'])
@@ -132,3 +215,26 @@ class AuthTest(DashboardTestCase):
         self._get("/api/host")
         self.assertStatus(200)
         self.delete_user("user")
+
+        # test with Cookies set
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self.create_user('user', 'user', ['read-only'])
+        time.sleep(1)
+        self._post("/api/auth", {'username': 'user', 'password': 'user'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(200)
+        time.sleep(1)
+        self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password', 'user'], 'user2')
+        time.sleep(1)
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self.set_jwt_token(None)
+        self._post("/api/auth", {'username': 'user', 'password': 'user2'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(200)
+        self.delete_user("user")
index 0353835b1f556b1b07e1a0a718850a3268a3877b..17e293e79d701a90f7906b565f6bd42712113282 100644 (file)
@@ -946,3 +946,12 @@ def allow_empty_body(func):  # noqa: N802
     except (AttributeError, KeyError):
         func._cp_config = {'tools.json_in.force': False}
     return func
+
+
+def set_cookies(url_prefix, token):
+    cherrypy.response.cookie['token'] = token
+    if url_prefix == 'https':
+        cherrypy.response.cookie['token']['secure'] = True
+    cherrypy.response.cookie['token']['HttpOnly'] = True
+    cherrypy.response.cookie['token']['path'] = '/'
+    cherrypy.response.cookie['token']['SameSite'] = 'Strict'
index ea7d6f627855920bedf2a11f87c1f4b69ca640be..8452b6432e89ab68064ab3b74f589f62b68d9779 100644 (file)
@@ -1,15 +1,20 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-import cherrypy
+import Cookie
+import sys
 import jwt
 
 from . import ApiController, RESTController, \
-    allow_empty_body
+    allow_empty_body, set_cookies
 from .. import logger, mgr
 from ..exceptions import DashboardException
 from ..services.auth import AuthManager, JwtManager
 from ..services.access_control import UserDoesNotExist
+# Python 3.8 introduced `samesite` attribute:
+# https://docs.python.org/3/library/http.cookies.html#morsel-objects
+if sys.version_info < (3, 8):
+    Cookie.Morsel._reserved["samesite"] = "SameSite"  # type: ignore  # pylint: disable=W0212
 
 
 @ApiController('/auth', secure=False)
@@ -21,10 +26,11 @@ class Auth(RESTController):
     def create(self, username, password):
         user_perms = AuthManager.authenticate(username, password)
         if user_perms is not None:
+            url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http'
             logger.debug('Login successful')
             token = JwtManager.gen_token(username)
             token = token.decode('utf-8')
-            cherrypy.response.headers['Authorization'] = "Bearer: {}".format(token)
+            set_cookies(url_prefix, token)
             return {
                 'token': token,
                 'username': username,
index d4c33783587e992912fbaac5eb631e16244feafe..f94d81f6dc2678d5afabbbd343db59e2d5e67c5e 100644 (file)
@@ -374,8 +374,11 @@ class Docs(BaseController):
             spec_url = "{}/docs/api.json".format(base)
 
         auth_header = cherrypy.request.headers.get('authorization')
+        auth_cookie = cherrypy.request.cookie['token']
         jwt_token = ""
-        if auth_header is not None:
+        if auth_cookie is not None:
+            jwt_token = auth_cookie.value
+        elif auth_header is not None:
             scheme, params = auth_header.split(' ', 1)
             if scheme.lower() == 'bearer':
                 jwt_token = params
index f007f691cc5b9f7caea3af9c3d310e404a3e531e..f02f81fe312de7d8c93004063c4fdf63a215a14b 100644 (file)
@@ -17,7 +17,7 @@ from .. import mgr
 from ..exceptions import UserDoesNotExist
 from ..services.auth import JwtManager
 from ..tools import prepare_url_prefix
-from . import Controller, Endpoint, BaseController
+from . import BaseController, Controller, Endpoint, allow_empty_body, set_cookies
 
 
 @Controller('/auth/saml2', secure=False)
@@ -46,6 +46,7 @@ class Saml2(BaseController):
             raise cherrypy.HTTPError(400, 'Single Sign-On is not configured.')
 
     @Endpoint('POST', path="")
+    @allow_empty_body
     def auth_response(self, **kwargs):
         Saml2._check_python_saml()
         req = Saml2._build_req(self._request, kwargs)
@@ -73,6 +74,7 @@ class Saml2(BaseController):
             token = JwtManager.gen_token(username)
             JwtManager.set_user(JwtManager.decode_token(token))
             token = token.decode('utf-8')
+            set_cookies(url_prefix, token)
             raise cherrypy.HTTPRedirect("{}/#/login?access_token={}".format(url_prefix, token))
         else:
             return {
@@ -106,5 +108,6 @@ class Saml2(BaseController):
         # pylint: disable=unused-argument
         Saml2._check_python_saml()
         JwtManager.reset_user()
+        cherrypy.response.cookie['token'] = {'expires': 0, 'max-age': 0}
         url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
         raise cherrypy.HTTPRedirect("{}/#/login".format(url_prefix))
index 62ab5808b4d755b8148543e8b832a84901f8e584..a85e72678a8a74c9318ea9b748acb90b98eec003 100644 (file)
         "tslib": "^1.9.0"
       }
     },
-    "@auth0/angular-jwt": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-2.1.0.tgz",
-      "integrity": "sha512-1KFtqswmJeM8JiniagSenpwHKTf9l+W+TmfsWV+x9SoZIShc6YmBsZDxd+oruZJL7MbJlxIJ3SQs7Yl1wraQdg==",
-      "requires": {
-        "url": "^0.11.0"
-      }
-    },
     "@babel/code-frame": {
       "version": "7.5.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
index cc4fbc32b7d6ffbaa7780ac8d0ccef07cbcf663c..7e8ed52ffd30acc351b63aa72f39c13bad84d494 100644 (file)
@@ -34,7 +34,7 @@
     "test:config": "if [ ! -e 'src/unit-test-configuration.ts' ]; then cp 'src/unit-test-configuration.ts.sample' 'src/unit-test-configuration.ts'; fi",
     "e2e": "npm run env_build && npm run e2e:update && ng e2e --webdriverUpdate=false",
     "e2e:dev": "npm run env_build && npm run e2e:update && ng e2e --dev-server-target --webdriverUpdate=false",
-    "e2e:update": "npx webdriver-manager update --gecko=false --versions.chrome=$(google-chrome --version | awk '{ print $3 }')",
+    "e2e:update": "npx webdriver-manager update --ignore_ssl --gecko=false --versions.chrome=$(google-chrome --version | awk '{ print $3 }')",
     "lint:tslint": "ng lint",
     "lint:prettier": "prettier --list-different \"{src,e2e}/**/*.{ts,scss}\"",
     "lint:html": "html-linter --config html-linter.config.json",
@@ -86,7 +86,6 @@
     "@angular/platform-browser": "7.2.6",
     "@angular/platform-browser-dynamic": "7.2.6",
     "@angular/router": "7.2.6",
-    "@auth0/angular-jwt": "2.1.0",
     "@ngx-translate/i18n-polyfill": "1.0.0",
     "@swimlane/ngx-datatable": "14.0.0",
     "awesome-bootstrap-checkbox": "0.3.7",
index b05168d276f8e96c8824d8270543cf8dd2c39870..7b63f06b7bd1c7029e958ff65879a79000af40fd 100644 (file)
@@ -9,7 +9,6 @@ import {
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 
-import { JwtModule } from '@auth0/angular-jwt';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import { BlockUIModule } from 'ng-block-ui';
 import { AccordionModule } from 'ngx-bootstrap/accordion';
@@ -27,10 +26,6 @@ import { SharedModule } from './shared/shared.module';
 
 import { environment } from '../environments/environment';
 
-export function jwtTokenGetter() {
-  return localStorage.getItem('access_token');
-}
-
 @NgModule({
   declarations: [AppComponent],
   imports: [
@@ -49,12 +44,7 @@ export function jwtTokenGetter() {
     CephModule,
     AccordionModule.forRoot(),
     BsDropdownModule.forRoot(),
-    TabsModule.forRoot(),
-    JwtModule.forRoot({
-      config: {
-        tokenGetter: jwtTokenGetter
-      }
-    })
+    TabsModule.forRoot()
   ],
   exports: [SharedModule],
   providers: [
index 5eaed1c1ef64c813a4161f2b98a766baa118c180..9007e1a751d19c3016a0b512b2d259e364fed5d6 100644 (file)
@@ -88,7 +88,7 @@ describe('RbdSnapshotListComponent', () => {
       rbdService = new RbdService(null, null);
       notificationService = new NotificationService(null, null, null);
       authStorageService = new AuthStorageService();
-      authStorageService.set('user', '', { 'rbd-image': ['create', 'read', 'update', 'delete'] });
+      authStorageService.set('user', { 'rbd-image': ['create', 'read', 'update', 'delete'] });
       component = new RbdSnapshotListComponent(
         authStorageService,
         null,
index 6eee431b957d8a993959663a6c6e58612821d3c5..05609f1c14dd48ffcc7954f551514952a74c90a9 100644 (file)
@@ -52,7 +52,7 @@ export class LoginComponent implements OnInit {
             window.location.replace(login.login_url);
           }
         } else {
-          this.authStorageService.set(login.username, token, login.permissions);
+          this.authStorageService.set(login.username, login.permissions);
           this.router.navigate(['']);
         }
       });
index bf7f1b574e1d880df7231a9884f6e50b76494f52..366522c3883269ba2ea0e524020a43ad629d57f4 100644 (file)
@@ -3,7 +3,6 @@ import { Component, OnInit, ViewChild } from '@angular/core';
 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 
 import { CephReleaseNamePipe } from '../../../shared/pipes/ceph-release-name.pipe';
-import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import { SummaryService } from '../../../shared/services/summary.service';
 import { AboutComponent } from '../about/about.component';
 
@@ -21,8 +20,7 @@ export class DashboardHelpComponent implements OnInit {
   constructor(
     private summaryService: SummaryService,
     private cephReleaseNamePipe: CephReleaseNamePipe,
-    private modalService: BsModalService,
-    private authStorageService: AuthStorageService
+    private modalService: BsModalService
   ) {}
 
   ngOnInit() {
@@ -45,8 +43,6 @@ export class DashboardHelpComponent implements OnInit {
   }
 
   goToApiDocs() {
-    const tokenInput = this.docsFormElement.nativeElement.children[0];
-    tokenInput.value = this.authStorageService.getToken();
     this.docsFormElement.nativeElement.submit();
   }
 }
index e7ff555705d6c57a3b440e836deb7e61cb15f1a6..b535547f6f5ab09362c7ad1d8308efcedce4d523 100644 (file)
@@ -33,7 +33,7 @@ describe('AuthService', () => {
 
   it('should login and save the user', fakeAsync(() => {
     const fakeCredentials = { username: 'foo', password: 'bar' };
-    const fakeResponse = { username: 'foo', token: 'tokenbytes' };
+    const fakeResponse = { username: 'foo' };
     service.login(<any>fakeCredentials);
     const req = httpTesting.expectOne('api/auth');
     expect(req.request.method).toBe('POST');
@@ -41,7 +41,6 @@ describe('AuthService', () => {
     req.flush(fakeResponse);
     tick();
     expect(localStorage.getItem('dashboard_username')).toBe('foo');
-    expect(localStorage.getItem('access_token')).toBe('tokenbytes');
   }));
 
   it('should logout and remove the user', fakeAsync(() => {
index fc940081f2e99c2ed830cc2b42f99d1ea97b5113..fbee7b2028d55058f4f03afc82da084bdc1c05eb 100644 (file)
@@ -26,14 +26,14 @@ export class AuthService {
       .post('api/auth', credentials)
       .toPromise()
       .then((resp: LoginResponse) => {
-        this.authStorageService.set(resp.username, resp.token, resp.permissions);
+        this.authStorageService.set(resp.username, resp.permissions);
       });
   }
 
   logout(callback: Function = null) {
     return this.http.post('api/auth/logout', null).subscribe((resp: any) => {
-      this.router.navigate(['/logout'], { skipLocationChange: true });
       this.authStorageService.remove();
+      this.router.navigate(['/logout'], { skipLocationChange: true });
       if (callback) {
         callback();
       }
index 4e8c5d17f88fcea1c157009c32fd54df0ec92eea..15d3197c77bff64a8ef7a2e76f6837beb75c21ac 100644 (file)
@@ -1,5 +1,4 @@
 export class LoginResponse {
   username: string;
-  token: string;
   permissions: object;
 }
index e9726a47e10cb2e2e833fc7532bbdd5bf30528e9..068a1e1d6a0a1ed398f677b6fef0a162c5a24fad 100644 (file)
@@ -13,18 +13,18 @@ describe('AuthStorageService', () => {
   });
 
   it('should store username', () => {
-    service.set(username, '');
+    service.set(username);
     expect(localStorage.getItem('dashboard_username')).toBe(username);
   });
 
   it('should remove username', () => {
-    service.set(username, '');
+    service.set(username);
     service.remove();
     expect(localStorage.getItem('dashboard_username')).toBe(null);
   });
 
   it('should be loggedIn', () => {
-    service.set(username, '');
+    service.set(username);
     expect(service.isLoggedIn()).toBe(true);
   });
 
index cab5bc813ca36109eef46f20452da89b8e3d6603..1f34eeedca09a49ece3c3cb69ce07d0b11acc520 100644 (file)
@@ -8,21 +8,15 @@ import { Permissions } from '../models/permissions';
 export class AuthStorageService {
   constructor() {}
 
-  set(username: string, token: string, permissions: object = {}) {
+  set(username: string, permissions: object = {}) {
     localStorage.setItem('dashboard_username', username);
-    localStorage.setItem('access_token', token);
     localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
   }
 
   remove() {
-    localStorage.removeItem('access_token');
     localStorage.removeItem('dashboard_username');
   }
 
-  getToken(): string {
-    return localStorage.getItem('access_token');
-  }
-
   isLoggedIn() {
     return localStorage.getItem('dashboard_username') !== null;
   }
index d4439416d987302bf0d05838dc12c9a01a49e682..76a0a0f82a1f631a2ec7ad023e5d71ec620161ad 100644 (file)
@@ -48,7 +48,7 @@ describe('SummaryService', () => {
 
   it('should call refresh', fakeAsync(() => {
     summaryService.enablePolling();
-    authStorageService.set('foobar', undefined, undefined);
+    authStorageService.set('foobar', undefined);
     const calledWith = [];
     summaryService.subscribe((data) => {
       calledWith.push(data);
@@ -65,7 +65,7 @@ describe('SummaryService', () => {
 
   describe('Should test methods after first refresh', () => {
     beforeEach(() => {
-      authStorageService.set('foobar', undefined, undefined);
+      authStorageService.set('foobar', undefined);
       summaryService.refresh();
     });
 
index 25e2963450e542f766c1c3b069069183d0efe113..280debdcb03e3f1907767940e4745c61d9034038 100755 (executable)
@@ -66,11 +66,11 @@ fi
 cd $DASH_DIR/frontend
 jq .[].target=$BASE_URL proxy.conf.json.sample > proxy.conf.json
 
-. $BUILD_DIR/src/pybind/mgr/dashboard/node-env/bin/activate
+[ -z $(command -v npm) ] && . $BUILD_DIR/src/pybind/mgr/dashboard/node-env/bin/activate
 npm ci
 
 if [ $DEVICE == "chrome" ]; then
-    npm run e2e || stop 1
+    npm run e2e -- --dev-server-target --baseUrl=$(echo $BASE_URL | tr -d '"') || stop 1
     stop 0
 elif [ $DEVICE == "docker" ]; then
     failed=0
index be5967394d1e946a1a5864f4852f6a4770479eb2..8b35e227537c4085050324161890a402b7498161 100644 (file)
@@ -61,12 +61,20 @@ class JwtManager(object):
 
     @classmethod
     def get_token_from_header(cls):
-        auth_header = cherrypy.request.headers.get('authorization')
-        if auth_header is not None:
-            scheme, params = auth_header.split(' ', 1)
-            if scheme.lower() == 'bearer':
-                return params
-        return None
+        auth_cookie_name = 'token'
+        try:
+            # use cookie
+            return cherrypy.request.cookie[auth_cookie_name].value
+        except KeyError:
+            try:
+                # fall-back: use Authorization header
+                auth_header = cherrypy.request.headers.get('authorization')
+                if auth_header is not None:
+                    scheme, params = auth_header.split(' ', 1)
+                    if scheme.lower() == 'bearer':
+                        return params
+            except IndexError:
+                return None
 
     @classmethod
     def set_user(cls, token):