diff --git a/qa/tasks/mgr/dashboard/helper.py b/qa/tasks/mgr/dashboard/helper.py index 9627a8428f84e..7f45cea3dbf1b 100644 --- a/qa/tasks/mgr/dashboard/helper.py +++ b/qa/tasks/mgr/dashboard/helper.py @@ -73,17 +73,20 @@ def create_user(cls, username, password, roles): 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 @@ -164,29 +167,49 @@ def setUp(self): 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. @@ -200,8 +223,8 @@ def _request(cls, url, method, data=None, params=None): 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): @@ -222,16 +245,16 @@ def _view_cache_get(cls, url, retries=5): 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): @@ -250,9 +273,9 @@ def _assertIsInst(cls, v1, v2): # 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 @@ -303,16 +326,16 @@ def _task_request(cls, method, url, data, timeout): 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): diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py index 0acc64478d17d..4b653e066180a 100644 --- a/qa/tasks/mgr/dashboard/test_auth.py +++ b/qa/tasks/mgr/dashboard/test_auth.py @@ -30,6 +30,7 @@ def _validate_jwt_token(self, token, username, permissions): self.assertIn('delete', perms) 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) @@ -37,13 +38,29 @@ def test_a_set_login_credentials(self): 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({ @@ -63,7 +80,17 @@ def test_login_without_password(self): }) self.delete_user('admin2') + # 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() @@ -78,7 +105,23 @@ def test_logout(self): 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) @@ -91,7 +134,21 @@ def test_token_ttl(self): 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) @@ -111,11 +168,37 @@ def test_remove_from_blacklist(self): 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']) @@ -137,3 +220,26 @@ def test_invalidate_token_by_admin(self): 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") diff --git a/src/auth/cephx/CephxServiceHandler.cc b/src/auth/cephx/CephxServiceHandler.cc index 2ab0602e25f61..97ebd10e5bbbc 100644 --- a/src/auth/cephx/CephxServiceHandler.cc +++ b/src/auth/cephx/CephxServiceHandler.cc @@ -200,11 +200,14 @@ int CephxServiceHandler::handle_request( } } encode(cbl, *result_bl); - // provite all of the other tickets at the same time + // provide requested service tickets at the same time vector info_vec; for (uint32_t service_id = 1; service_id <= req.other_keys; service_id <<= 1) { - if (req.other_keys & service_id) { + // skip CEPH_ENTITY_TYPE_AUTH: auth ticket is already encoded + // (possibly encrypted with the old session key) + if ((req.other_keys & service_id) && + service_id != CEPH_ENTITY_TYPE_AUTH) { ldout(cct, 10) << " adding key for service " << ceph_entity_type_name(service_id) << dendl; CephXSessionAuthInfo svc_info; @@ -264,7 +267,10 @@ int CephxServiceHandler::handle_request( int service_err = 0; for (uint32_t service_id = 1; service_id <= ticket_req.keys; service_id <<= 1) { - if (ticket_req.keys & service_id) { + // skip CEPH_ENTITY_TYPE_AUTH: auth ticket must be obtained with + // CEPHX_GET_AUTH_SESSION_KEY + if ((ticket_req.keys & service_id) && + service_id != CEPH_ENTITY_TYPE_AUTH) { ldout(cct, 10) << " adding key for service " << ceph_entity_type_name(service_id) << dendl; CephXSessionAuthInfo info; diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index 0353835b1f556..17e293e79d701 100644 --- a/src/pybind/mgr/dashboard/controllers/__init__.py +++ b/src/pybind/mgr/dashboard/controllers/__init__.py @@ -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' diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py index ea7d6f6278559..8452b6432e89a 100644 --- a/src/pybind/mgr/dashboard/controllers/auth.py +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -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, diff --git a/src/pybind/mgr/dashboard/controllers/docs.py b/src/pybind/mgr/dashboard/controllers/docs.py index d4c33783587e9..f94d81f6dc267 100644 --- a/src/pybind/mgr/dashboard/controllers/docs.py +++ b/src/pybind/mgr/dashboard/controllers/docs.py @@ -374,8 +374,11 @@ def _swagger_ui_page(self, all_endpoints=False, token=None): 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 diff --git a/src/pybind/mgr/dashboard/controllers/saml2.py b/src/pybind/mgr/dashboard/controllers/saml2.py index f007f691cc5b9..f02f81fe312de 100644 --- a/src/pybind/mgr/dashboard/controllers/saml2.py +++ b/src/pybind/mgr/dashboard/controllers/saml2.py @@ -17,7 +17,7 @@ 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 @@ def _check_python_saml(): 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 @@ def auth_response(self, **kwargs): 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 @@ def logout(self, **kwargs): # 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)) diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index 62ab5808b4d75..a85e72678a8a7 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -482,14 +482,6 @@ "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", diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index 0c3d1ecbfd1b8..470702e47aa66 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -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", diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts index b05168d276f8e..7b63f06b7bd1c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts @@ -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: [ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts index 5eaed1c1ef64c..9007e1a751d19 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts @@ -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, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts index 108663be985fd..41d0260e9c0f4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts @@ -48,7 +48,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(['']); } }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts index bf7f1b574e1d8..366522c388326 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts @@ -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(); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts index e7ff555705d6c..b535547f6f5ab 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts @@ -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(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(() => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts index fc940081f2e99..fbee7b2028d55 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts @@ -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(); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts index 4e8c5d17f88fc..15d3197c77bff 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts @@ -1,5 +1,4 @@ export class LoginResponse { username: string; - token: string; permissions: object; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts index e9726a47e10cb..068a1e1d6a0a1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts @@ -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); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts index cab5bc813ca36..1f34eeedca09a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts @@ -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; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts index d4439416d9873..76a0a0f82a1f6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts @@ -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(); }); diff --git a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh index 87a1e6b4697b4..12ffe3e218fc6 100755 --- a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh +++ b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh @@ -62,11 +62,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 diff --git a/src/pybind/mgr/dashboard/services/auth.py b/src/pybind/mgr/dashboard/services/auth.py index be5967394d1e9..8b35e227537c4 100644 --- a/src/pybind/mgr/dashboard/services/auth.py +++ b/src/pybind/mgr/dashboard/services/auth.py @@ -61,12 +61,20 @@ def decode_token(cls, token): @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): diff --git a/src/rgw/rgw_cors.cc b/src/rgw/rgw_cors.cc index 0b3e4f3945561..bfe83d6420ed9 100644 --- a/src/rgw/rgw_cors.cc +++ b/src/rgw/rgw_cors.cc @@ -148,8 +148,9 @@ void RGWCORSRule::format_exp_headers(string& s) { if (s.length() > 0) s.append(","); // these values are sent to clients in a 'Access-Control-Expose-Headers' - // response header, so we escape '\n' to avoid header injection - boost::replace_all_copy(std::back_inserter(s), header, "\n", "\\n"); + // response header, so we escape '\n' and '\r' to avoid header injection + std::string tmp = boost::replace_all_copy(header, "\n", "\\n"); + boost::replace_all_copy(std::back_inserter(s), tmp, "\r", "\\r"); } } diff --git a/src/rgw/rgw_rest_swift.cc b/src/rgw/rgw_rest_swift.cc index 6c6d7b55f3e85..e1d8095ebdbfc 100644 --- a/src/rgw/rgw_rest_swift.cc +++ b/src/rgw/rgw_rest_swift.cc @@ -2545,6 +2545,9 @@ bool RGWSwiftWebsiteHandler::is_web_dir() const return false; } else if (subdir_name.back() == '/') { subdir_name.pop_back(); + if (subdir_name.empty()) { + return false; + } } rgw_obj obj(s->bucket, std::move(subdir_name));