]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/auth.js
LP#1774448 Auth poll spam/timing repairs
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / auth.js
1 /* Core Sevice - egAuth
2  *
3  * Manages login and auth session retrieval.
4  */
5
6 angular.module('egCoreMod')
7
8 .factory('egAuth', 
9        ['$q','$timeout','$rootScope','$window','$location','egNet','egHatch',
10 function($q , $timeout , $rootScope , $window , $location , egNet , egHatch) {
11
12     var service = {
13         // the currently active user (au) object
14         user : function(u) {
15             if (u) {
16                 this._user = u;
17             }
18             return this._user;
19         },
20
21         // the user hidden by an operator change
22         OCuser : function(u) {
23             if (u) {
24                 this._OCuser = u;
25             }
26             return this._OCuser;
27         },
28
29         // the Op Change hidden auth token string
30         OCtoken : function() {
31             return egHatch.getLoginSessionItem('eg.auth.token.oc');
32         },
33
34         // Op Change hidden authtime in seconds
35         OCauthtime : function() {
36             return egHatch.getLoginSessionItem('eg.auth.time.oc');
37         },
38
39         // the currently active auth token string
40         token : function() {
41             return egHatch.getLoginSessionItem('eg.auth.token');
42         },
43
44         // authtime in seconds
45         authtime : function() {
46             return egHatch.getLoginSessionItem('eg.auth.time');
47         },
48
49         // the currently active workstation name
50         // For ws_ou or wsid(), see egAuth.user().ws_ou(), etc.
51         workstation : function() {
52             return this.ws;
53         },
54
55         // Listen for logout events in other tabs
56         // Current version of phantomjs (unit tests, etc.) does not 
57         // support BroadcastChannel, so just dummy it up.
58         authChannel : (typeof BroadcastChannel == 'undefined') ? 
59             {} : new BroadcastChannel('eg.auth')
60     };
61
62     /* Returns a promise, which is resolved if valid
63      * authtoken is found, otherwise rejected */
64     service.testAuthToken = function() {
65         var deferred = $q.defer();
66         var token = service.token();
67
68         if (token) {
69
70             if (lf.isOffline && !$location.path().match(/\/session/) ) {
71                 // Just stop here if we're in the offline interface but not on the session tab
72                 $timeout(function(){deferred.resolve()});
73             } else if (lf.isOffline && $location.path().match(/\/session/) && !$window.navigator.onLine) {
74                 // Likewise, if we're in the offline interface on the session tab and the network is down.
75                 // The session tab itself will redirect appropriately due to no network.
76                 $timeout(function(){deferred.resolve()});
77             } else {
78                 // Otherwise, check the token.  This will freeze all other interfaces, which is what we want.
79                 egNet.request(
80                     'open-ils.auth',
81                     'open-ils.auth.session.retrieve', token)
82     
83                 .then(function(user) {
84                     if (user && user.classname) {
85                         // authtoken test succeeded
86                         service.user(user);
87                         service.poll();
88                         service.check_workstation(deferred);
89     
90                     } else {
91                         // authtoken test failed
92                         egHatch.clearLoginSessionItems();
93                         deferred.reject(); 
94                     }
95                 });
96             }
97
98         } else {
99             // no authtoken to test
100             deferred.reject('No authtoken found');
101         }
102
103         return deferred.promise;
104     };
105
106     service.check_workstation = function(deferred) {
107
108         var user = service.user();
109         var ws_path = '/admin/workstation/workstations';
110
111         return egHatch.getItem('eg.workstation.all')
112         .then(function(workstations) { 
113             if (!workstations) workstations = [];
114
115             // If the user is authenticated with a workstation, get the
116             // name from the locally registered version of the workstation.
117
118             if (user.wsid()) {
119
120                 var ws = workstations.filter(
121                     function(w) {return w.id == user.wsid()})[0];
122
123                 if (ws) { // success
124                     service.ws = ws.name;
125                     deferred.resolve();
126                     return;
127                 }
128             }
129
130             if ($location.path() == ws_path) {
131                 // User is on the workstation admin page.  No need
132                 // to redirect.
133                 deferred.resolve();
134                 return;
135             }
136
137             // At this point, the user is trying to access a page
138             // besides the workstation admin page without a valid
139             // registered workstation.  Send them back to the 
140             // workstation admin page.
141
142             // NOTE: egEnv also defines basePath, but we cannot import
143             // egEnv here becuase it creates a circular reference.
144             $window.location.href = '/eg/staff' + ws_path;
145             deferred.resolve();
146         });
147     }
148
149     /**
150      * Returns a promise, which is resolved on successful 
151      * login and rejected on failed login.
152      */
153     service.login = function(args, ops) {
154         // avoid modifying the caller's data structure.
155         args = angular.copy(args);
156
157         if (!ops) { // only set on redo attempts.
158             ops = {deferred : $q.defer()};
159
160             // Clear old LoginSession keys that were left in localStorage
161             // when the previous user closed the browser without logging
162             // out.  Under normal circumstance, LoginSession data would
163             // have been cleared by now, either during logout or cookie
164             // expiration.  But, if for some reason the user manually
165             // removed the auth token cookie w/o closing the browser
166             // (say, for testing), then this serves double duty to ensure
167             // LoginSession data cannot persist across logins.
168             egHatch.clearLoginSessionItems();
169         }
170
171         service.login_api(args).then(function(evt) {
172
173             if (evt.textcode == 'SUCCESS') {
174                 service.handle_login_ok(args, evt);
175                 ops.deferred.resolve({
176                     invalid_workstation : ops.invalid_workstation
177                 });
178
179             } else if (evt.textcode == 'WORKSTATION_NOT_FOUND') {
180                 ops.invalid_workstation = true;
181                 delete args.workstation;
182                 service.login(args, ops); // redo w/o workstation
183
184             } else {
185                 // note: the likely outcome here is a NO_SESION
186                 // server event, which results in broadcasting an 
187                 // egInvalidAuth by egNet. 
188                 console.error('login failed ' + js2JSON(evt));
189                 ops.deferred.reject();
190             }
191         });
192
193         return ops.deferred.promise;
194     }
195
196     /**
197      * Returns a promise, which is resolved on successful 
198      * login and rejected on failed login.
199      */
200     service.opChange = function(args) {
201         // avoid modifying the caller's data structure.
202         args = angular.copy(args);
203         args.workstation = service.workstation();
204
205         var deferred = $q.defer();
206
207         service.login_api(args).then(function(evt) {
208
209             if (evt.textcode == 'SUCCESS') {
210                 if (args.type != 'persist') {
211                     egHatch.setLoginSessionItem('eg.auth.token.oc', service.token());
212                     egHatch.setLoginSessionItem('eg.auth.time.oc', service.authtime());
213                     service.OCuser(service.user());
214                 }
215                 service.handle_login_ok(args, evt);
216                 service.testAuthToken().then(
217                     deferred.resolve,
218                     function () { service.opChangeUndo().then(deferred.reject)  }
219                 );
220             } else {
221                 // note: the likely outcome here is a NO_SESION
222                 // server event, which results in broadcasting an 
223                 // egInvalidAuth by egNet. 
224                 console.error('operator change failed ' + js2JSON(evt));
225                 deferred.reject();
226             }
227         });
228
229         return deferred.promise;
230     }
231
232     service.opChangeUndo = function() {
233         if (service.OCtoken()) {
234             service.user(service.OCuser());
235             egHatch.setLoginSessionItem('eg.auth.token', service.OCtoken());
236             egHatch.setLoginSessionItem('eg.auth.time', service.OCauthtime());
237             egHatch.removeLoginSessionItem('eg.auth.token.oc');
238             egHatch.removeLoginSessionItem('eg.auth.time.oc');
239         }
240         return service.testAuthToken();
241     }
242
243     service.login_api = function(args) {
244         return egNet.request(
245             'open-ils.auth',
246             'open-ils.auth.authenticate.init', args.username)
247         .then(function(seed) {
248                 // avoid clobbering the bare password in case
249                 // we need it for a login redo attempt.
250                 var login_args = angular.copy(args);
251                 login_args.password = hex_md5(seed + hex_md5(args.password));
252
253                 return egNet.request(
254                     'open-ils.auth',
255                     'open-ils.auth.authenticate.complete', login_args)
256             }
257         );
258     }
259
260     service.handle_login_ok = function(args, evt) {
261         service.ws = args.workstation; 
262         egHatch.setLoginSessionItem('eg.auth.token', evt.payload.authtoken);
263         egHatch.setLoginSessionItem('eg.auth.time', evt.payload.authtime);
264         service.poll();
265     }
266
267     /**
268      * Force-check the validity of the authtoken on occasion. 
269      * This allows us to redirect an idle staff client back to the login
270      * page after the session times out.  Otherwise, the UI would stay
271      * open with potentially sensitive data visible.
272      * TODO: What is the practical difference (for a browser) between 
273      * checking auth validity and the ui.general.idle_timeout setting?
274      * Does that setting serve a purpose in a browser environment?
275      */
276     service.poll = function() {
277
278         if (!service.authChannel.onmessage) {
279             // Now that we have an authtoken, listen for logout events 
280             // initiated by other tabs.
281             service.authChannel.onmessage = function(e) {
282                 if (e.data.action == 'logout') {
283                     $rootScope.$broadcast(
284                         'egAuthExpired', {startedElsewhere : true});
285                 }
286             }
287         }
288
289         // add a 5 second delay to give the token plenty of time
290         // to expire on the server.
291         var pollTime = service.authtime() * 1000 + 5000;
292
293         if (pollTime < 60000) {
294             // Never poll more often than once per minute.
295             pollTime = 60000;
296         } else if (pollTime > 2147483647) {
297             // Avoid integer overflow resulting in $timeout() effectively
298             // running with timeout=0 in a loop.
299             pollTime = 2147483647;
300         }
301
302         $timeout(
303             function() {
304                 egNet.request(                                                     
305                     'open-ils.auth',                                               
306                     'open-ils.auth.session.retrieve', 
307                     service.token(),
308                     0, // return extra auth details, unneeded here.
309                     1  // avoid extending the auth timeout
310                 ).then(function(user) {
311                     if (user && user.classname) { // all good
312                         service.poll();
313                     } else {
314                         // NOTE: we should never get here, since egNet
315                         // filters responses for NO_SESSION events.
316                         $rootScope.$broadcast('egAuthExpired');
317                     }
318                 })
319             },
320             pollTime
321         );
322     }
323
324     service.logout = function(broadcast) {
325
326         if (broadcast && service.authChannel.postMessage) {
327             // Tell the other tabs to shut it all down.
328             service.authChannel.postMessage({action : 'logout'});
329         }
330
331         if (service.token()) {
332             egNet.request(
333                 'open-ils.auth', 
334                 'open-ils.auth.session.delete', 
335                 service.token()); // fire and forget
336             egHatch.clearLoginSessionItems();
337         }
338         service._user = null;
339     };
340
341     return service;
342 }])
343
344
345 /**
346  * Service for testing user permissions.
347  * Note: this cannot live within egAuth, because it creates a circular
348  * dependency of egOrg -> egEnv -> egAuth -> egOrg
349  */
350 .factory('egPerm', 
351        ['$q','egNet','egAuth','egOrg',
352 function($q , egNet , egAuth , egOrg) {
353     var service = {};
354
355     /*
356      * Returns the full list of org unit objects at which the currently
357      * logged in user has the selected permissions.
358      * @permList - list or string.  If a list, the response object is a
359      * hash of perm => orgList maps.  If a string, the response is the
360      * org list for the requested perm.
361      */
362     service.hasPermAt = function(permList, asId) {
363         var deferred = $q.defer();
364         var isArray = true;
365         if (!angular.isArray(permList)) {
366             isArray = false;
367             permList = [permList];
368         }
369         // as called, this method will return the top-most org unit of the
370         // sub-tree at which this user has the selected permission.
371         // From there, flesh the descendant orgs locally.
372         egNet.request(
373             'open-ils.actor',
374             'open-ils.actor.user.has_work_perm_at.batch',
375             egAuth.token(), permList
376         ).then(function(resp) {
377             var answer = {};
378             angular.forEach(permList, function(perm) {
379                 var all = [];
380                 angular.forEach(resp[perm], function(oneOrg) {
381                     all = all.concat(egOrg.descendants(oneOrg, asId));
382                 });
383                 answer[perm] = all;
384             });
385             if (!isArray) answer = answer[permList[0]];
386             deferred.resolve(answer);
387         });
388        return deferred.promise;
389     };
390
391
392     /**
393      * Returns a hash of perm => hasPermBool for each requested permission.
394      * If the authenticated user has no workstation, no checks are made
395      * and all permissions return false.
396      */
397     service.hasPermHere = function(permList) {
398         var response = {};
399
400         var isArray = true;
401         if (!angular.isArray(permList)) {
402             isArray = false;
403             permList = [permList];
404         }
405
406         // no workstation, all are false
407         if (egAuth.user().wsid() === null) {
408             console.warn("egPerm.hasPermHere() called with no workstation");
409             if (isArray) {
410                 response = permList.map(function(perm) {
411                     return response[perm] = false;
412                 });
413             } else {
414                 response = false;
415             }
416             return $q.when(response);
417         }
418
419         ws_ou = Number(egAuth.user().ws_ou()); // from string
420
421         return service.hasPermAt(permList, true)
422         .then(function(orgMap) {
423             angular.forEach(orgMap, function(orgIds, perm) {
424                 // each permission is mapped to a flat list of org unit ids,
425                 // including descendants.  See if our workstation org unit
426                 // is in the list.
427                 response[perm] = orgIds.indexOf(ws_ou) > -1;
428             });
429             if (!isArray) response = response[permList[0]];
430             return response;
431         });
432     }
433
434     return service;
435 }])
436
437