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