]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/core/auth.service.ts
LP1860460 Copy delete override repairs, perm failed handler
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / core / auth.service.ts
1 import {Injectable, EventEmitter} from '@angular/core';
2 import {NetService} from './net.service';
3 import {EventService, EgEvent} from './event.service';
4 import {IdlService, IdlObject} from './idl.service';
5 import {StoreService} from './store.service';
6
7 // Not universally available.
8 declare var BroadcastChannel;
9
10 // Models a login instance.
11 class AuthUser {
12     user:        IdlObject; // actor.usr (au) object
13     workstation: string; // workstation name
14     token:       string;
15     authtime:    number;
16
17     constructor(token: string, authtime: number, workstation?: string) {
18         this.token = token;
19         this.workstation = workstation;
20         this.authtime = authtime;
21     }
22 }
23
24 // Params required for calling the login() method.
25 interface AuthLoginArgs {
26     username: string;
27     password: string;
28     type: string;
29     workstation?: string;
30 }
31
32 export enum AuthWsState {
33     PENDING,
34     NOT_USED,
35     NOT_FOUND_SERVER,
36     NOT_FOUND_LOCAL,
37     VALID
38 }
39
40 @Injectable({providedIn: 'root'})
41 export class AuthService {
42
43     private authChannel: any;
44
45     private activeUser: AuthUser = null;
46
47     workstationState: AuthWsState = AuthWsState.PENDING;
48
49     // Used by auth-checking resolvers
50     redirectUrl: string;
51
52     // reference to active auth validity setTimeout handler.
53     pollTimeout: any;
54
55     constructor(
56         private egEvt: EventService,
57         private net: NetService,
58         private store: StoreService
59     ) {
60
61         // BroadcastChannel is not yet defined in PhantomJS and elsewhere
62         this.authChannel = (typeof BroadcastChannel === 'undefined') ?
63             {} : new BroadcastChannel('eg.auth');
64     }
65
66     // Returns true if we are currently in op-change mode.
67     opChangeIsActive(): boolean {
68         return Boolean(this.store.getLoginSessionItem('eg.auth.time.oc'));
69     }
70
71     // - Accessor functions always refer to the active user.
72
73     user(): IdlObject {
74         return this.activeUser ? this.activeUser.user : null;
75     }
76
77     // Workstation name.
78     workstation(): string {
79         return this.activeUser ? this.activeUser.workstation : null;
80     }
81
82     token(): string {
83         return this.activeUser ? this.activeUser.token : null;
84     }
85
86     authtime(): number {
87         return this.activeUser ? this.activeUser.authtime : 0;
88     }
89
90     // NOTE: NetService emits an event if the auth session has expired.
91     // This only rejects when no authtoken is found.
92     testAuthToken(): Promise<any> {
93
94         if (!this.activeUser) {
95             // Only necessary on new page loads.  During op-change,
96             // for example, we already have an activeUser.
97             this.activeUser = new AuthUser(
98                 this.store.getLoginSessionItem('eg.auth.token'),
99                 this.store.getLoginSessionItem('eg.auth.time')
100             );
101         }
102
103         if (!this.token()) {
104             return Promise.reject('no authtoken');
105         }
106
107         return this.net.request(
108             'open-ils.auth',
109             'open-ils.auth.session.retrieve', this.token()).toPromise()
110         .then(user => {
111             // NetService interceps NO_SESSION events.
112             // We can only get here if the session is valid.
113             this.activeUser.user = user;
114             this.listenForLogout();
115             this.sessionPoll();
116         });
117     }
118
119     loginApi(args: AuthLoginArgs, service: string,
120         method: string, isOpChange?: boolean): Promise<void> {
121
122         return this.net.request(service, method, args)
123         .toPromise().then(res => {
124             return this.handleLoginResponse(
125                 args, this.egEvt.parse(res), isOpChange);
126         });
127     }
128
129     login(args: AuthLoginArgs, isOpChange?: boolean): Promise<void> {
130         let service = 'open-ils.auth';
131         let method = 'open-ils.auth.login';
132
133         if (isOpChange && this.opChangeIsActive()) {
134             // Enforce one op-change at a time.
135             this.undoOpChange();
136         }
137
138         return this.net.request(
139             'open-ils.auth_proxy',
140             'open-ils.auth_proxy.enabled')
141         .toPromise().then(
142             enabled => {
143                 if (Number(enabled) === 1) {
144                     service = 'open-ils.auth_proxy';
145                     method = 'open-ils.auth_proxy.login';
146                 }
147                 return this.loginApi(args, service, method, isOpChange);
148             },
149             error => {
150                 // auth_proxy check resulted in a low-level error.
151                 // Likely the service is not running.  Fall back to
152                 // standard auth login.
153                 return this.loginApi(args, service, method, isOpChange);
154             }
155         );
156     }
157
158     handleLoginResponse(
159         args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
160
161         switch (evt.textcode) {
162             case 'SUCCESS':
163                 return this.handleLoginOk(args, evt, isOpChange);
164
165             case 'WORKSTATION_NOT_FOUND':
166                 console.error(`No such workstation "${args.workstation}"`);
167                 this.workstationState = AuthWsState.NOT_FOUND_SERVER;
168                 delete args.workstation;
169                 return this.login(args, isOpChange);
170
171             default:
172                 console.error(`Login returned unexpected event: ${evt}`);
173                 return Promise.reject('login failed');
174         }
175     }
176
177     // Stash the login data
178     handleLoginOk(args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
179
180         if (isOpChange) {
181             this.store.setLoginSessionItem('eg.auth.token.oc', this.token());
182             this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime());
183         }
184
185         this.activeUser = new AuthUser(
186             evt.payload.authtoken,
187             evt.payload.authtime,
188             args.workstation
189         );
190
191         this.store.setLoginSessionItem('eg.auth.token', this.token());
192         this.store.setLoginSessionItem('eg.auth.time', this.authtime());
193
194         return Promise.resolve();
195     }
196
197     undoOpChange(): Promise<any> {
198         if (this.opChangeIsActive()) {
199             this.deleteSession();
200             this.activeUser = new AuthUser(
201                 this.store.getLoginSessionItem('eg.auth.token.oc'),
202                 this.store.getLoginSessionItem('eg.auth.time.oc'),
203                 this.activeUser.workstation
204             );
205             this.store.removeLoginSessionItem('eg.auth.token.oc');
206             this.store.removeLoginSessionItem('eg.auth.time.oc');
207             this.store.setLoginSessionItem('eg.auth.token', this.token());
208             this.store.setLoginSessionItem('eg.auth.time', this.authtime());
209         }
210         // Re-fetch the user.
211         return this.testAuthToken();
212     }
213
214     /**
215      * Listen for logout events initiated by other browser tabs.
216      */
217     listenForLogout(): void {
218         if (this.authChannel.onmessage) {
219             return;
220         }
221
222         this.authChannel.onmessage = (e) => {
223             console.debug(
224                 `received eg.auth broadcast ${JSON.stringify(e.data)}`);
225
226             if (e.data.action === 'logout') {
227                 // Logout will be handled by the originating tab.
228                 // We just need to clear tab-local memory.
229                 this.cleanup();
230                 this.net.authExpired$.emit({viaExternal: true});
231             }
232         };
233     }
234
235     /**
236      * Force-check the validity of the authtoken on occasion.
237      * This allows us to redirect an idle staff client back to the login
238      * page after the session times out.  Otherwise, the UI would stay
239      * open with potentially sensitive data visible.
240      * TODO: What is the practical difference (for a browser) between
241      * checking auth validity and the ui.general.idle_timeout setting?
242      * Does that setting serve a purpose in a browser environment?
243      */
244     sessionPoll(): void {
245
246         // add a 5 second delay to give the token plenty of time
247         // to expire on the server.
248         let pollTime = this.authtime() * 1000 + 5000;
249
250         if (pollTime < 60000) {
251             // Never poll more often than once per minute.
252             pollTime = 60000;
253         } else if (pollTime > 2147483647) {
254             // Avoid integer overflow resulting in $timeout() effectively
255             // running with timeout=0 in a loop.
256             pollTime = 2147483647;
257         }
258
259         this.pollTimeout = setTimeout(() => {
260             this.net.request(
261                 'open-ils.auth',
262                 'open-ils.auth.session.retrieve',
263                 this.token(),
264                 0, // return extra auth details, unneeded here.
265                 1  // avoid extending the auth timeout
266
267             // NetService intercepts NO_SESSION events.
268             // If the promise resolves, the session is valid.
269             ).subscribe(
270                 user => this.sessionPoll(),
271                 err  => console.warn('auth poll error: ' + err)
272             );
273
274         }, pollTime);
275     }
276
277
278     // Resolves if login workstation matches a workstation known to this
279     // browser instance.  No attempt is made to see if the workstation
280     // is present on the server.  That happens at login time.
281     verifyWorkstation(): Promise<void> {
282
283         if (!this.user()) {
284             this.workstationState = AuthWsState.PENDING;
285             return Promise.reject('Cannot verify workstation without user');
286         }
287
288         if (!this.user().wsid()) {
289             this.workstationState = AuthWsState.NOT_USED;
290             return Promise.reject('User has no workstation ID to verify');
291         }
292
293         return new Promise((resolve, reject) => {
294             return this.store.getWorkstations().then(workstations => {
295
296                 if (workstations) {
297                     const ws = workstations.filter(
298                         w => Number(w.id) === Number(this.user().wsid()))[0];
299
300                     if (ws) {
301                         this.activeUser.workstation = ws.name;
302                         this.workstationState = AuthWsState.VALID;
303                         return resolve();
304                     }
305                 }
306
307                 this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
308                 reject();
309             });
310         });
311     }
312
313     deleteSession(): void {
314         if (this.token()) {
315             // note we have to subscribe to the net.request or it will
316             // not fire -- observables only run when subscribed to.
317             this.net.request(
318                 'open-ils.auth',
319                 'open-ils.auth.session.delete', this.token())
320             .subscribe(x => {});
321         }
322     }
323
324     // Tell all listening browser tabs that it's time to logout.
325     // This should only be invoked by one tab.
326     broadcastLogout(): void {
327         console.debug('Notifying tabs of imminent auth token removal');
328         this.authChannel.postMessage({action : 'logout'});
329     }
330
331     // Remove/reset session data
332     cleanup(): void {
333         this.activeUser = null;
334         if (this.pollTimeout) {
335             clearTimeout(this.pollTimeout);
336             this.pollTimeout = null;
337         }
338     }
339
340     // Invalidate server auth token and clean up.
341     logout(): void {
342         this.deleteSession();
343         this.store.clearLoginSessionItems();
344         this.cleanup();
345     }
346 }