]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/core/auth.service.ts
LP1819179 PCRUD selector fleshing handles maps
[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         return this.net.request(
134             'open-ils.auth_proxy',
135             'open-ils.auth_proxy.enabled')
136         .toPromise().then(
137             enabled => {
138                 if (Number(enabled) === 1) {
139                     service = 'open-ils.auth_proxy';
140                     method = 'open-ils.auth_proxy.login';
141                 }
142                 return this.loginApi(args, service, method, isOpChange);
143             },
144             error => {
145                 // auth_proxy check resulted in a low-level error.
146                 // Likely the service is not running.  Fall back to
147                 // standard auth login.
148                 return this.loginApi(args, service, method, isOpChange);
149             }
150         );
151     }
152
153     handleLoginResponse(
154         args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
155
156         switch (evt.textcode) {
157             case 'SUCCESS':
158                 return this.handleLoginOk(args, evt, isOpChange);
159
160             case 'WORKSTATION_NOT_FOUND':
161                 console.error(`No such workstation "${args.workstation}"`);
162                 this.workstationState = AuthWsState.NOT_FOUND_SERVER;
163                 delete args.workstation;
164                 return this.login(args, isOpChange);
165
166             default:
167                 console.error(`Login returned unexpected event: ${evt}`);
168                 return Promise.reject('login failed');
169         }
170     }
171
172     // Stash the login data
173     handleLoginOk(args: AuthLoginArgs, evt: EgEvent, isOpChange: boolean): Promise<void> {
174
175         if (isOpChange) {
176             this.store.setLoginSessionItem('eg.auth.token.oc', this.token());
177             this.store.setLoginSessionItem('eg.auth.time.oc', this.authtime());
178         }
179
180         this.activeUser = new AuthUser(
181             evt.payload.authtoken,
182             evt.payload.authtime,
183             args.workstation
184         );
185
186         this.store.setLoginSessionItem('eg.auth.token', this.token());
187         this.store.setLoginSessionItem('eg.auth.time', this.authtime());
188
189         return Promise.resolve();
190     }
191
192     undoOpChange(): Promise<any> {
193         if (this.opChangeIsActive()) {
194             this.deleteSession();
195             this.activeUser = new AuthUser(
196                 this.store.getLoginSessionItem('eg.auth.token.oc'),
197                 this.store.getLoginSessionItem('eg.auth.time.oc'),
198                 this.activeUser.workstation
199             );
200             this.store.removeLoginSessionItem('eg.auth.token.oc');
201             this.store.removeLoginSessionItem('eg.auth.time.oc');
202             this.store.setLoginSessionItem('eg.auth.token', this.token());
203             this.store.setLoginSessionItem('eg.auth.time', this.authtime());
204         }
205         // Re-fetch the user.
206         return this.testAuthToken();
207     }
208
209     /**
210      * Listen for logout events initiated by other browser tabs.
211      */
212     listenForLogout(): void {
213         if (this.authChannel.onmessage) {
214             return;
215         }
216
217         this.authChannel.onmessage = (e) => {
218             console.debug(
219                 `received eg.auth broadcast ${JSON.stringify(e.data)}`);
220
221             if (e.data.action === 'logout') {
222                 // Logout will be handled by the originating tab.
223                 // We just need to clear tab-local memory.
224                 this.cleanup();
225                 this.net.authExpired$.emit({viaExternal: true});
226             }
227         };
228     }
229
230     /**
231      * Force-check the validity of the authtoken on occasion.
232      * This allows us to redirect an idle staff client back to the login
233      * page after the session times out.  Otherwise, the UI would stay
234      * open with potentially sensitive data visible.
235      * TODO: What is the practical difference (for a browser) between
236      * checking auth validity and the ui.general.idle_timeout setting?
237      * Does that setting serve a purpose in a browser environment?
238      */
239     sessionPoll(): void {
240
241         // add a 5 second delay to give the token plenty of time
242         // to expire on the server.
243         let pollTime = this.authtime() * 1000 + 5000;
244
245         if (pollTime < 60000) {
246             // Never poll more often than once per minute.
247             pollTime = 60000;
248         } else if (pollTime > 2147483647) {
249             // Avoid integer overflow resulting in $timeout() effectively
250             // running with timeout=0 in a loop.
251             pollTime = 2147483647;
252         }
253
254         this.pollTimeout = setTimeout(() => {
255             this.net.request(
256                 'open-ils.auth',
257                 'open-ils.auth.session.retrieve',
258                 this.token(),
259                 0, // return extra auth details, unneeded here.
260                 1  // avoid extending the auth timeout
261
262             // NetService intercepts NO_SESSION events.
263             // If the promise resolves, the session is valid.
264             ).subscribe(
265                 user => this.sessionPoll(),
266                 err  => console.warn('auth poll error: ' + err)
267             );
268
269         }, pollTime);
270     }
271
272
273     // Resolves if login workstation matches a workstation known to this
274     // browser instance.  No attempt is made to see if the workstation
275     // is present on the server.  That happens at login time.
276     verifyWorkstation(): Promise<void> {
277
278         if (!this.user()) {
279             this.workstationState = AuthWsState.PENDING;
280             return Promise.reject('Cannot verify workstation without user');
281         }
282
283         if (!this.user().wsid()) {
284             this.workstationState = AuthWsState.NOT_USED;
285             return Promise.reject('User has no workstation ID to verify');
286         }
287
288         return new Promise((resolve, reject) => {
289             const workstations =
290                 this.store.getLocalItem('eg.workstation.all');
291
292             if (workstations) {
293                 const ws = workstations.filter(
294                     w => Number(w.id) === Number(this.user().wsid()))[0];
295
296                 if (ws) {
297                     this.activeUser.workstation = ws.name;
298                     this.workstationState = AuthWsState.VALID;
299                     return resolve();
300                 }
301             }
302
303             this.workstationState = AuthWsState.NOT_FOUND_LOCAL;
304             reject();
305         });
306     }
307
308     deleteSession(): void {
309         if (this.token()) {
310             // note we have to subscribe to the net.request or it will
311             // not fire -- observables only run when subscribed to.
312             this.net.request(
313                 'open-ils.auth',
314                 'open-ils.auth.session.delete', this.token())
315             .subscribe(x => {});
316         }
317     }
318
319     // Tell all listening browser tabs that it's time to logout.
320     // This should only be invoked by one tab.
321     broadcastLogout(): void {
322         console.debug('Notifying tabs of imminent auth token removal');
323         this.authChannel.postMessage({action : 'logout'});
324     }
325
326     // Remove/reset session data
327     cleanup(): void {
328         this.activeUser = null;
329         if (this.pollTimeout) {
330             clearTimeout(this.pollTimeout);
331             this.pollTimeout = null;
332         }
333     }
334
335     // Invalidate server auth token and clean up.
336     logout(): void {
337         this.deleteSession();
338         this.store.clearLoginSessionItems();
339         this.cleanup();
340     }
341 }