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
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.
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):
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):
# 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
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):
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)
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({
"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()
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)
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)
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'])
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")
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'
# -*- 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)
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,
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
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)
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)
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 {
# 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))
"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",
"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",
"@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",
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';
import { environment } from '../environments/environment';
-export function jwtTokenGetter() {
- return localStorage.getItem('access_token');
-}
-
@NgModule({
declarations: [AppComponent],
imports: [
CephModule,
AccordionModule.forRoot(),
BsDropdownModule.forRoot(),
- TabsModule.forRoot(),
- JwtModule.forRoot({
- config: {
- tokenGetter: jwtTokenGetter
- }
- })
+ TabsModule.forRoot()
],
exports: [SharedModule],
providers: [
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,
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(['']);
}
});
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';
constructor(
private summaryService: SummaryService,
private cephReleaseNamePipe: CephReleaseNamePipe,
- private modalService: BsModalService,
- private authStorageService: AuthStorageService
+ private modalService: BsModalService
) {}
ngOnInit() {
}
goToApiDocs() {
- const tokenInput = this.docsFormElement.nativeElement.children[0];
- tokenInput.value = this.authStorageService.getToken();
this.docsFormElement.nativeElement.submit();
}
}
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');
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(() => {
.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();
}
export class LoginResponse {
username: string;
- token: string;
permissions: object;
}
});
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);
});
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;
}
it('should call refresh', fakeAsync(() => {
summaryService.enablePolling();
- authStorageService.set('foobar', undefined, undefined);
+ authStorageService.set('foobar', undefined);
const calledWith = [];
summaryService.subscribe((data) => {
calledWith.push(data);
describe('Should test methods after first refresh', () => {
beforeEach(() => {
- authStorageService.set('foobar', undefined, undefined);
+ authStorageService.set('foobar', undefined);
summaryService.refresh();
});
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
@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):