From: Bill Erickson Date: Tue, 26 Dec 2017 20:39:35 +0000 (-0500) Subject: LP#626157 sessionPoll; cleanup X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=b10c942ee77360a959cf71857cadd15509f9c278;p=working%2FEvergreen.git LP#626157 sessionPoll; cleanup Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/eg2-src/src/app/core/auth.ts b/Open-ILS/eg2-src/src/app/core/auth.ts index aba9deab05..ac2120676f 100644 --- a/Open-ILS/eg2-src/src/app/core/auth.ts +++ b/Open-ILS/eg2-src/src/app/core/auth.ts @@ -1,13 +1,15 @@ import {Injectable, EventEmitter} from '@angular/core'; -import {Observable} from 'rxjs/Rx'; import {EgNetService} from './net'; import {EgEventService, EgEvent} from './event'; import {EgIdlService, EgIdlObject} from './idl'; import {EgStoreService} from './store'; +// Not universally available. +declare var BroadcastChannel; + // Models a login instance. class EgAuthUser { - user: EgIdlObject; + user: EgIdlObject; // actor.usr (au) object workstation: string; // workstation name token: string; authtime: number; @@ -38,24 +40,36 @@ export enum EgAuthWsState { @Injectable() export class EgAuthService { + private authChannel: any; + private activeUser: EgAuthUser; // opChangeUser refers to the user that has been superseded during - // an op-change event. This use will become the activeUser once - // again, when the op-change cycle has completed. + // an op-change event. opChangeUser resumes its status as the + // activeUser once the op-change cycle has completed. private opChangeUser: EgAuthUser; workstationState: EgAuthWsState = EgAuthWsState.PENDING; + // Used by auth-checking resolvers redirectUrl: string; + // reference to active auth validity setTimeout handler. + pollTimeout: any; + constructor( private egEvt: EgEventService, private net: EgNetService, private store: EgStoreService - ) {} + ) { + + // BroadcastChannel is not yet defined in PhantomJS + this.authChannel = BroadcastChannel ? + new BroadcastChannel('eg.auth') : {}; + } + - // - Accessor functions alway refer to the active user. + // - Accessor functions always refer to the active user. user(): EgIdlObject { return this.activeUser ? this.activeUser.user : null; @@ -63,15 +77,15 @@ export class EgAuthService { // Workstation name. workstation(): string { - return this.activeUser.workstation; + return this.activeUser ? this.activeUser.workstation : null; }; token(): string { return this.activeUser ? this.activeUser.token : null; }; - authtime(): Number { - return this.activeUser.authtime + authtime(): number { + return this.activeUser ? this.activeUser.authtime : 0; }; // NOTE: EgNetService emits an event if the auth session has expired. @@ -84,40 +98,24 @@ export class EgAuthService { if (!this.token()) return Promise.reject('no authtoken'); - return new Promise( (resolve, reject) => { - this.net.request( - 'open-ils.auth', - 'open-ils.auth.session.retrieve', this.token() - ).subscribe( - user => { - // EgNetService interceps NO_SESSION events. - // We can only get here if the session is valid. - this.activeUser.user = user; - this.sessionPoll(); - resolve(); - }, - err => { reject(); } - ); + return this.net.request( + 'open-ils.auth', + 'open-ils.auth.session.retrieve', this.token()).toPromise() + .then(user => { + // EgNetService interceps NO_SESSION events. + // We can only get here if the session is valid. + this.activeUser.user = user; + this.listenForLogout(); + this.sessionPoll(); }); } - checkWorkstation(): void { - // TODO: - // Emits event on invalid workstation. - } - login(args: EgAuthLoginArgs, isOpChange?: boolean): Promise { - - return new Promise((resolve, reject) => { - this.net.request('open-ils.auth', 'open-ils.auth.login', args) - .subscribe(res => { - this.handleLoginResponse(args, this.egEvt.parse(res), isOpChange) - .then( - ok => resolve(ok), - notOk => reject(notOk) - ); - }); - }); + return this.net.request('open-ils.auth', 'open-ils.auth.login', args) + .toPromise().then(res => { + return this.handleLoginResponse( + args, this.egEvt.parse(res), isOpChange) + }) } handleLoginResponse( @@ -172,42 +170,89 @@ export class EgAuthService { return this.testAuthToken(); } + /** + * Listen for logout events initiated by other browser tabs. + */ + listenForLogout(): void { + if (this.authChannel.onmessage) return; + + this.authChannel.onmessage = (e) => { + console.debug( + `received eg.auth broadcast ${JSON.stringify(e.data)}`); + + if (e.data.action == 'logout') { + // Logout will be handled by the originating tab. + // We just need to clear tab-local memory. + this.cleanup(); + this.net.authExpired$.emit({viaExternal: true}); + } + } + } + + /** + * Force-check the validity of the authtoken on occasion. + * This allows us to redirect an idle staff client back to the login + * page after the session times out. Otherwise, the UI would stay + * open with potentially sensitive data visible. + * TODO: What is the practical difference (for a browser) between + * checking auth validity and the ui.general.idle_timeout setting? + * Does that setting serve a purpose in a browser environment? + */ sessionPoll(): void { - // TODO + + // add a 5 second delay to give the token plenty of time + // to expire on the server. + let pollTime = this.authtime() * 1000 + 5000; + + console.debug('EgAuth session poll at ' + pollTime); + + this.pollTimeout = setTimeout(() => { + this.net.request( + 'open-ils.auth', + 'open-ils.auth.session.retrieve', + this.token(), + 0, // return extra auth details, unneeded here. + 1 // avoid extending the auth timeout + + // EgNetService intercepts NO_SESSION events. + // If the promise resolves, the session is valid. + ).toPromise().then(user => this.sessionPoll()) + + }, pollTime); } + // Resolves if login workstation matches a workstation known to this // browser instance. verifyWorkstation(): Promise { - return new Promise((resolve, reject) => { - if (!this.user()) { - this.workstationState = EgAuthWsState.PENDING; - reject(); - return; - } + if (!this.user()) { + this.workstationState = EgAuthWsState.PENDING; + return Promise.reject('Cannot verify workstation without user'); + } - if (!this.user().wsid()) { - this.workstationState = EgAuthWsState.NOT_USED; - reject(); - return; - } + if (!this.user().wsid()) { + this.workstationState = EgAuthWsState.NOT_USED; + return Promise.reject('User has no workstation ID to verify'); + } + return new Promise((resolve, reject) => { this.store.getItem('eg.workstation.all') .then(workstations => { - if (!workstations) workstations = []; - - let ws = workstations.filter( - w => {return w.id == this.user().wsid()})[0]; - - if (ws) { - this.activeUser.workstation = ws.name; - this.workstationState = EgAuthWsState.VALID; - resolve(); - } else { - this.workstationState = EgAuthWsState.NOT_FOUND_LOCAL; - reject(); + + if (workstations) { + let ws = workstations.filter( + w => {return w.id == this.user().wsid()})[0]; + + if (ws) { + this.activeUser.workstation = ws.name; + this.workstationState = EgAuthWsState.VALID; + return resolve(); + } } + + this.workstationState = EgAuthWsState.NOT_FOUND_LOCAL; + reject(); }); }); } @@ -221,17 +266,27 @@ export class EgAuthService { } } - logout(broadcast?: boolean) { - console.debug('logging out'); + // Tell all listening browser tabs that it's time to logout. + // This should only be invoked by one tab. + broadcastLogout(): void { + console.debug('Notifying tabs of imminent auth token removal'); + this.authChannel.postMessage({action : 'logout'}); + } - if (broadcast) { - // TODO - //this.authChannel.postMessage({action : 'logout'}); + // Remove/reset session data + cleanup(): void { + this.activeUser = null; + this.opChangeUser = null; + if (this.pollTimeout) { + clearTimeout(this.pollTimeout); + this.pollTimeout = null; } + } + // Invalidate server auth token and clean up. + logout(): void { this.deleteSession(); this.store.clearLoginSessionItems(); - this.activeUser = null; - this.opChangeUser = null; + this.cleanup(); } } diff --git a/Open-ILS/eg2-src/src/app/core/event.ts b/Open-ILS/eg2-src/src/app/core/event.ts index 3f6afc7d30..33e3f84697 100644 --- a/Open-ILS/eg2-src/src/app/core/event.ts +++ b/Open-ILS/eg2-src/src/app/core/event.ts @@ -1,18 +1,18 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; export class EgEvent { - code : Number; - textcode : String; + code : number; + textcode : string; payload : any; - desc : String; - debug : String; - note : String; - servertime : String; - ilsperm : String; - ilspermloc : Number; + desc : string; + debug : string; + note : string; + servertime : string; + ilsperm : string; + ilspermloc : number; success : Boolean = false; - toString(): String { + toString(): string { let s = `Event: ${this.code}:${this.textcode} -> ${this.desc}`; if (this.ilsperm) s += ` ${this.ilsperm}@${this.ilspermloc}`; @@ -39,8 +39,8 @@ export class EgEventService { .forEach(field => { evt[field] = thing[field]; }); evt.debug = thing.stacktrace; - evt.code = new Number(thing.code); - evt.ilspermloc = new Number(thing.ilspermloc); + evt.code = +(thing.ilsevent || -1); + evt.ilspermloc = +(thing.ilspermloc || -1); evt.success = thing.textcode == 'SUCCESS'; return evt; diff --git a/Open-ILS/eg2-src/src/app/core/net.ts b/Open-ILS/eg2-src/src/app/core/net.ts index 121601c49b..502ffbf0de 100644 --- a/Open-ILS/eg2-src/src/app/core/net.ts +++ b/Open-ILS/eg2-src/src/app/core/net.ts @@ -15,9 +15,9 @@ * Each response is relayed via Observable onNext(). The interface is * the same for streaming and atomic requests. */ -import { Injectable, EventEmitter } from '@angular/core'; -import { Observable, Observer } from 'rxjs/Rx'; -import { EgEventService, EgEvent } from './event'; +import {Injectable, EventEmitter} from '@angular/core'; +import {Observable, Observer} from 'rxjs/Rx'; +import {EgEventService, EgEvent} from './event'; // Global vars from opensrf.js // These are availavble at runtime, but are not exported. @@ -31,6 +31,8 @@ export class EgNetRequest { superseded : Boolean = false; // If set, this will be used instead of a one-off OpenSRF.ClientSession. session? : any; + // True if we're using a single-use local session + localSession: boolean = true; // Last EgEvent encountered by this request. // Most callers will not need to import EgEvent since the parsed @@ -43,17 +45,28 @@ export class EgNetRequest { this.params = params; if (session) { this.session = session; + this.localSession = false; } else { this.session = new OpenSRF.ClientSession(service); } } } +export interface EgAuthExpiredEvent { + // request is set when the auth expiration was determined as a + // by-product of making an API call. + request?: EgNetRequest; + + // True if this environment (e.g. browser tab) was notified of the + // expired auth token from an external source (e.g. another browser tab). + viaExternal?: boolean; +} + @Injectable() export class EgNetService { permFailed$: EventEmitter; - authExpired$: EventEmitter; + authExpired$: EventEmitter; // If true, permission failures are emitted via permFailed$ // and the active request is marked as superseded. @@ -63,7 +76,7 @@ export class EgNetService { private egEvt: EgEventService ) { this.permFailed$ = new EventEmitter(); - this.authExpired$ = new EventEmitter(); + this.authExpired$ = new EventEmitter(); } // Standard request call -- Variadic params version @@ -97,6 +110,13 @@ export class EgNetService { method : request.method, params : request.params, oncomplete : () => { + + // TODO: teach opensrf.js to call cleanup() inside + // disconnect() and teach EgPcrud to call cleanup() + // as needed to avoid long-lived session data bloat. + if (request.localSession) + request.session.cleanup(); + // A superseded request will be complete()'ed by the // superseder at a later time. if (!request.superseded) @@ -117,7 +137,7 @@ export class EgNetService { if (request.service == 'open-ils.pcrud' && statCode == 401) { // 401 is the PCRUD equivalent of a NO_SESSION event - this.authExpired$.emit(request); + this.authExpired$.emit({request: request}); } request.observer.error(msg); @@ -126,8 +146,9 @@ export class EgNetService { }).send(); } - // Relay response object to the caller for typical/successful responses. - // Applies special handling to response events that require global attention. + // Relay response object to the caller for typical/successful + // responses. Applies special handling to response events that + // require global attention. private dispatchResponse(request, response): void { request.evt = this.egEvt.parse(response); @@ -137,7 +158,7 @@ export class EgNetService { case 'NO_SESSION': console.debug(`EgNet emitting event: ${request.evt}`); request.observer.error(request.evt.toString()); - this.authExpired$.emit(request); + this.authExpired$.emit({request: request}); return; case 'PERM_FAILURE': diff --git a/Open-ILS/eg2-src/src/app/core/pcrud.ts b/Open-ILS/eg2-src/src/app/core/pcrud.ts index 0cee7d3073..643a515915 100644 --- a/Open-ILS/eg2-src/src/app/core/pcrud.ts +++ b/Open-ILS/eg2-src/src/app/core/pcrud.ts @@ -1,6 +1,5 @@ import {Injectable} from '@angular/core'; import {Observable, Observer} from 'rxjs/Rx'; -//import {toPromise} from 'rxjs/operators'; import {EgIdlService, EgIdlObject} from './idl'; import {EgNetService, EgNetRequest} from './net'; import {EgAuthService} from './auth'; @@ -158,15 +157,13 @@ export class EgPcrudContext { // => xact_close(commit/rollback) // => disconnect wrapXact(mainFunc: () => Observable): Observable { - let this_ = this; - return Observable.create(observer => { // 1. connect this.connect() // 2. start the transaction - .then(() => {return this_.xactBegin().toPromise()}) + .then(() => {return this.xactBegin().toPromise()}) // 3. execute the main body .then(() => { @@ -175,9 +172,9 @@ export class EgPcrudContext { res => observer.next(res), err => observer.error(err), () => { - this_.xactClose().toPromise().then(() => { + this.xactClose().toPromise().then(() => { // 5. disconnect - this_.disconnect(); + this.disconnect(); // 6. all done observer.complete(); }); @@ -209,12 +206,10 @@ export class EgPcrudContext { if (!Array.isArray(list)) this.cudList = [list]; - let this_ = this; - return this.wrapXact(() => { return Observable.create(observer => { - this_.cudObserver = observer; - this_.nextCudRequest(); + this.cudObserver = observer; + this.nextCudRequest(); }); }); } @@ -225,8 +220,6 @@ export class EgPcrudContext { * all are done, the cudObserver is resolved. */ nextCudRequest(): void { - let this_ = this; - if (this.cudIdx >= this.cudList.length) { this.cudObserver.complete(); return; @@ -250,9 +243,9 @@ export class EgPcrudContext { `open-ils.pcrud.${action}.${fmObj.classname}`, [this.token(), fmObj] ).subscribe( - res => this_.cudObserver.next(res), - err => this_.cudObserver.error(err), - () => this_.nextCudRequest() + res => this.cudObserver.next(res), + err => this.cudObserver.error(err), + () => this.nextCudRequest() ); }; } diff --git a/Open-ILS/eg2-src/src/app/staff/app.component.ts b/Open-ILS/eg2-src/src/app/staff/app.component.ts index 6f1dbdab55..27a60f9806 100644 --- a/Open-ILS/eg2-src/src/app/staff/app.component.ts +++ b/Open-ILS/eg2-src/src/app/staff/app.component.ts @@ -33,7 +33,12 @@ export class EgStaffComponent implements OnInit { }); // Redirect to the login page on any auth timeout events. - this.net.authExpired$.subscribe(uhOh => { + this.net.authExpired$.subscribe(expireEvent => { + + // If the expired authtoken was identified locally (i.e. + // in this browser tab) notify all tabs of imminent logout. + if (!expireEvent.viaExternal) this.auth.broadcastLogout(); + console.debug('Auth session has expired. Redirecting to login'); this.auth.redirectUrl = this.router.url; this.router.navigate([this.loginPath]); @@ -41,8 +46,7 @@ export class EgStaffComponent implements OnInit { this.route.data.subscribe((data: {staffResolver : any}) => { console.debug('EgStaff ngOnInit complete'); - - }); + }); } /** diff --git a/Open-ILS/eg2-src/src/app/staff/app.module.ts b/Open-ILS/eg2-src/src/app/staff/app.module.ts index 6cefff5698..39561db86e 100644 --- a/Open-ILS/eg2-src/src/app/staff/app.module.ts +++ b/Open-ILS/eg2-src/src/app/staff/app.module.ts @@ -32,6 +32,7 @@ import {EgConfirmDialogComponent} from '@eg/share/confirm-dialog.component'; // Components available to all staff/sub modules EgOrgSelectComponent, EgConfirmDialogComponent, + CommonModule, FormsModule, NgbModule ] diff --git a/Open-ILS/eg2-src/src/app/staff/login.component.ts b/Open-ILS/eg2-src/src/app/staff/login.component.ts index fadcfc25e4..85d0490f25 100644 --- a/Open-ILS/eg2-src/src/app/staff/login.component.ts +++ b/Open-ILS/eg2-src/src/app/staff/login.component.ts @@ -27,8 +27,9 @@ export class EgStaffLoginComponent implements OnInit { private auth: EgAuthService, private store: EgStoreService ) {} - + ngOnInit() { + console.debug('login ngOnInit()'); // clear out any stale auth data this.auth.logout(); diff --git a/Open-ILS/eg2-src/src/app/staff/nav.component.html b/Open-ILS/eg2-src/src/app/staff/nav.component.html index f325a7410a..f85a6e1232 100644 --- a/Open-ILS/eg2-src/src/app/staff/nav.component.html +++ b/Open-ILS/eg2-src/src/app/staff/nav.component.html @@ -191,7 +191,7 @@ more_vert