From: Ricardo Dias Date: Tue, 3 Jul 2018 10:34:11 +0000 (+0100) Subject: mgr/dashboard: frontend: JWT authentication implementation X-Git-Tag: v14.1.0~1053^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=9c36996b1e8f9cce11ca71f697805551064e5f72;p=ceph.git mgr/dashboard: frontend: JWT authentication implementation Signed-off-by: Ricardo Dias --- diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index a7e597617e18..29a7140a0e50 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -374,6 +374,14 @@ "tslib": "^1.9.0" } }, + "@auth0/angular-jwt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-2.0.0.tgz", + "integrity": "sha512-RVlXFpcqQ+9uCpzboU7Tm1ubaRVO2FrR5+RYuwHtTT4BXquVMEwOSbAuuaArFud/kMc00XYoGgiP1JkCfOAfpA==", + "requires": { + "url": "^0.11.0" + } + }, "@babel/code-frame": { "version": "7.0.0-beta.56", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.56.tgz", @@ -4170,12 +4178,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4190,17 +4200,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4317,7 +4330,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4329,6 +4343,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4343,6 +4358,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4350,12 +4366,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4374,6 +4392,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4454,7 +4473,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4466,6 +4486,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4587,6 +4608,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9001,8 +9023,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", @@ -11077,7 +11098,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, "requires": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -11086,8 +11106,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index 7f388544b512..1dfb7154ef80 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -16,7 +16,6 @@ "fix:prettier": "prettier --write \"{src,e2e}/**/*.{ts,scss}\"", "fix:tslint": "npm run lint:tslint -- --fix", "fix": "npm run fix:tslint; npm run fix:prettier" - }, "private": true, "jest": { @@ -52,6 +51,7 @@ "@angular/platform-browser": "6.1.4", "@angular/platform-browser-dynamic": "6.1.4", "@angular/router": "6.1.4", + "@auth0/angular-jwt": "^2.0.0", "@swimlane/ngx-datatable": "13.1.0", "@types/lodash": "4.14.116", "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 2026b8ada9ce..47a4e44ad017 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts @@ -3,6 +3,7 @@ import { ErrorHandler, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { JwtModule } from '@auth0/angular-jwt'; import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr'; import { AccordionModule } from 'ngx-bootstrap/accordion'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; @@ -23,6 +24,10 @@ export class CustomOption extends ToastOptions { enableHTML = true; } +export function jwtTokenGetter() { + return localStorage.getItem('access_token'); +} + @NgModule({ declarations: [AppComponent], imports: [ @@ -36,7 +41,12 @@ export class CustomOption extends ToastOptions { CephModule, AccordionModule.forRoot(), BsDropdownModule.forRoot(), - TabsModule.forRoot() + TabsModule.forRoot(), + JwtModule.forRoot({ + config: { + tokenGetter: jwtTokenGetter + } + }) ], exports: [SharedModule], providers: [ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html index a89d87ef1bb2..a3d1d19af326 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html @@ -51,18 +51,6 @@ - -
- - -
- + + +
API + (click)="goToApiDocs()">API
  • 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 0774e86ea3da..bf7f1b574e1d 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 @@ -1,8 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +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'; @@ -12,13 +13,16 @@ import { AboutComponent } from '../about/about.component'; styleUrls: ['./dashboard-help.component.scss'] }) export class DashboardHelpComponent implements OnInit { + @ViewChild('docsForm') + docsFormElement; docsUrl: string; modalRef: BsModalRef; constructor( private summaryService: SummaryService, private cephReleaseNamePipe: CephReleaseNamePipe, - private modalService: BsModalService + private modalService: BsModalService, + private authStorageService: AuthStorageService ) {} ngOnInit() { @@ -39,4 +43,10 @@ export class DashboardHelpComponent implements OnInit { openAboutModal() { this.modalRef = this.modalService.show(AboutComponent); } + + 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 bfe7fdb2929d..fd2d9d8f57b3 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 @@ -32,13 +32,15 @@ describe('AuthService', () => { 'should login and save the user', fakeAsync(() => { const fakeCredentials = { username: 'foo', password: 'bar' }; + const fakeResponse = { username: 'foo', token: 'tokenbytes' }; service.login(fakeCredentials); const req = httpTesting.expectOne('api/auth'); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(fakeCredentials); - req.flush(fakeCredentials); + req.flush(fakeResponse); tick(); expect(localStorage.getItem('dashboard_username')).toBe('foo'); + expect(localStorage.getItem('access_token')).toBe('tokenbytes'); }) ); 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 9954e346b1a8..68ed81f35b23 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 @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Credentials } from '../models/credentials'; +import { LoginResponse } from '../models/login-response'; import { AuthStorageService } from '../services/auth-storage.service'; import { ApiModule } from './api.module'; @@ -15,8 +16,8 @@ export class AuthService { return this.http .post('api/auth', credentials) .toPromise() - .then((resp: Credentials) => { - this.authStorageService.set(resp.username, resp.permissions); + .then((resp: LoginResponse) => { + this.authStorageService.set(resp.username, resp.token, resp.permissions); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts index 7e617d986797..2c2b7d76e399 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts @@ -1,6 +1,4 @@ export class Credentials { username: string; password: string; - permissions: any; - stay_signed_in = false; } 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 new file mode 100644 index 000000000000..4e8c5d17f88f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts @@ -0,0 +1,5 @@ +export class LoginResponse { + username: string; + token: string; + permissions: object; +} 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 86afaff40fba..b0053d0e3991 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 @@ -9,15 +9,21 @@ import { ServicesModule } from './services.module'; export class AuthStorageService { constructor() {} - set(username: string, permissions: any = {}) { + set(username: string, token: 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; }