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