]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/auth.js
LP#1755258 Browser client auth proxy login support
[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             if (evt.textcode == 'SUCCESS') {
173                 service.handle_login_ok(args, evt);
174                 ops.deferred.resolve({
175                     invalid_workstation : ops.invalid_workstation
176                 });
177
178             } else if (evt.textcode == 'WORKSTATION_NOT_FOUND') {
179                 ops.invalid_workstation = true;
180                 delete args.workstation;
181                 service.login(args, ops); // redo w/o workstation
182
183             } else {
184                 // note: the likely outcome here is a NO_SESION
185                 // server event, which results in broadcasting an 
186                 // egInvalidAuth by egNet. 
187                 console.error('login failed ' + js2JSON(evt));
188                 ops.deferred.reject();
189             }
190         });
191
192         return ops.deferred.promise;
193     }
194
195     /**
196      * Returns a promise, which is resolved on successful 
197      * login and rejected on failed login.
198      */
199     service.opChange = function(args) {
200         // avoid modifying the caller's data structure.
201         args = angular.copy(args);
202         args.workstation = service.workstation();
203
204         var deferred = $q.defer();
205
206         service.login_api(args).then(function(evt) {
207
208             if (evt.textcode == 'SUCCESS') {
209                 if (args.type != 'persist') {
210                     egHatch.setLoginSessionItem('eg.auth.token.oc', service.token());
211                     egHatch.setLoginSessionItem('eg.auth.time.oc', service.authtime());
212                     service.OCuser(service.user());
213                 }
214                 service.handle_login_ok(args, evt);
215                 service.testAuthToken().then(
216                     deferred.resolve,
217                     function () { service.opChangeUndo().then(deferred.reject)  }
218                 );
219             } else {
220                 // note: the likely outcome here is a NO_SESION
221                 // server event, which results in broadcasting an 
222                 // egInvalidAuth by egNet. 
223                 console.error('operator change failed ' + js2JSON(evt));
224                 deferred.reject();
225             }
226         });
227
228         return deferred.promise;
229     }
230
231     service.opChangeUndo = function() {
232         if (service.OCtoken()) {
233             service.user(service.OCuser());
234             egHatch.setLoginSessionItem('eg.auth.token', service.OCtoken());
235             egHatch.setLoginSessionItem('eg.auth.time', service.OCauthtime());
236             egHatch.removeLoginSessionItem('eg.auth.token.oc');
237             egHatch.removeLoginSessionItem('eg.auth.time.oc');
238         }
239         return service.testAuthToken();
240     }
241
242     service.login_via_auth_proxy = function(args) {
243         return egNet.request(
244             'open-ils.auth_proxy',
245             'open-ils.auth_proxy.login', args);
246     }
247
248     service.login_via_auth = function(args) {
249         return egNet.request(
250             'open-ils.auth',
251             'open-ils.auth.authenticate.init', args.username)
252         .then(function(seed) {
253                 // avoid clobbering the bare password in case
254                 // we need it for a login redo attempt.
255                 var login_args = angular.copy(args);
256                 login_args.password = hex_md5(seed + hex_md5(args.password));
257
258                 return egNet.request(
259                     'open-ils.auth',
260                     'open-ils.auth.authenticate.complete', login_args)
261             }
262         );
263     }
264
265     service.login_api = function(args) {
266
267         return egNet.request(
268             'open-ils.auth_proxy',
269             'open-ils.auth_proxy.enabled')
270         .then(
271             function(enabled) {
272                 console.log('proxy check returned ' + enabled);
273                 if (Number(enabled) === 1) {
274                     return service.login_via_auth_proxy(args);
275                 } else {
276                     return service.login_via_auth(args);
277                 }
278             },
279             function() {
280                 // request failed, likely a result of auth_proxy not running.
281                return service.login_via_auth(args);
282             }
283         );
284     }
285
286     service.handle_login_ok = function(args, evt) {
287         service.ws = args.workstation; 
288         egHatch.setLoginSessionItem('eg.auth.token', evt.payload.authtoken);
289         egHatch.setLoginSessionItem('eg.auth.time', evt.payload.authtime);
290         service.poll();
291     }
292
293     /**
294      * Force-check the validity of the authtoken on occasion. 
295      * This allows us to redirect an idle staff client back to the login
296      * page after the session times out.  Otherwise, the UI would stay
297      * open with potentially sensitive data visible.
298      * TODO: What is the practical difference (for a browser) between 
299      * checking auth validity and the ui.general.idle_timeout setting?
300      * Does that setting serve a purpose in a browser environment?
301      */
302     service.poll = function() {
303
304         if (!service.authChannel.onmessage) {
305             // Now that we have an authtoken, listen for logout events 
306             // initiated by other tabs.
307             service.authChannel.onmessage = function(e) {
308                 if (e.data.action == 'logout') {
309                     $rootScope.$broadcast(
310                         'egAuthExpired', {startedElsewhere : true});
311                 }
312             }
313         }
314
315         // add a 5 second delay to give the token plenty of time
316         // to expire on the server.
317         var pollTime = service.authtime() * 1000 + 5000;
318
319         if (pollTime < 60000) {
320             // Never poll more often than once per minute.
321             pollTime = 60000;
322         } else if (pollTime > 2147483647) {
323             // Avoid integer overflow resulting in $timeout() effectively
324             // running with timeout=0 in a loop.
325             pollTime = 2147483647;
326         }
327
328         $timeout(
329             function() {
330                 egNet.request(                                                     
331                     'open-ils.auth',                                               
332                     'open-ils.auth.session.retrieve', 
333                     service.token(),
334                     0, // return extra auth details, unneeded here.
335                     1  // avoid extending the auth timeout
336                 ).then(function(user) {
337                     if (user && user.classname) { // all good
338                         service.poll();
339                     } else {
340                         // NOTE: we should never get here, since egNet
341                         // filters responses for NO_SESSION events.
342                         $rootScope.$broadcast('egAuthExpired');
343                     }
344                 })
345             },
346             pollTime
347         );
348     }
349
350     service.logout = function(broadcast) {
351
352         if (broadcast && service.authChannel.postMessage) {
353             // Tell the other tabs to shut it all down.
354             service.authChannel.postMessage({action : 'logout'});
355         }
356
357         if (service.token()) {
358             egNet.request(
359                 'open-ils.auth', 
360                 'open-ils.auth.session.delete', 
361                 service.token()); // fire and forget
362             egHatch.clearLoginSessionItems();
363         }
364         service._user = null;
365     };
366
367     return service;
368 }])
369
370
371 /**
372  * Service for testing user permissions.
373  * Note: this cannot live within egAuth, because it creates a circular
374  * dependency of egOrg -> egEnv -> egAuth -> egOrg
375  */
376 .factory('egPerm', 
377        ['$q','egNet','egAuth','egOrg',
378 function($q , egNet , egAuth , egOrg) {
379     var service = {};
380
381     /*
382      * Returns the full list of org unit objects at which the currently
383      * logged in user has the selected permissions.
384      * @permList - list or string.  If a list, the response object is a
385      * hash of perm => orgList maps.  If a string, the response is the
386      * org list for the requested perm.
387      */
388     service.hasPermAt = function(permList, asId) {
389         var deferred = $q.defer();
390         var isArray = true;
391         if (!angular.isArray(permList)) {
392             isArray = false;
393             permList = [permList];
394         }
395         // as called, this method will return the top-most org unit of the
396         // sub-tree at which this user has the selected permission.
397         // From there, flesh the descendant orgs locally.
398         egNet.request(
399             'open-ils.actor',
400             'open-ils.actor.user.has_work_perm_at.batch',
401             egAuth.token(), permList
402         ).then(function(resp) {
403             var answer = {};
404             angular.forEach(permList, function(perm) {
405                 var all = [];
406                 angular.forEach(resp[perm], function(oneOrg) {
407                     all = all.concat(egOrg.descendants(oneOrg, asId));
408                 });
409                 answer[perm] = all;
410             });
411             if (!isArray) answer = answer[permList[0]];
412             deferred.resolve(answer);
413         });
414        return deferred.promise;
415     };
416
417
418     /**
419      * Returns a hash of perm => hasPermBool for each requested permission.
420      * If the authenticated user has no workstation, no checks are made
421      * and all permissions return false.
422      */
423     service.hasPermHere = function(permList) {
424         var response = {};
425
426         var isArray = true;
427         if (!angular.isArray(permList)) {
428             isArray = false;
429             permList = [permList];
430         }
431
432         // no workstation, all are false
433         if (egAuth.user().wsid() === null) {
434             console.warn("egPerm.hasPermHere() called with no workstation");
435             if (isArray) {
436                 response = permList.map(function(perm) {
437                     return response[perm] = false;
438                 });
439             } else {
440                 response = false;
441             }
442             return $q.when(response);
443         }
444
445         ws_ou = Number(egAuth.user().ws_ou()); // from string
446
447         return service.hasPermAt(permList, true)
448         .then(function(orgMap) {
449             angular.forEach(orgMap, function(orgIds, perm) {
450                 // each permission is mapped to a flat list of org unit ids,
451                 // including descendants.  See if our workstation org unit
452                 // is in the list.
453                 response[perm] = orgIds.indexOf(ws_ou) > -1;
454             });
455             if (!isArray) response = response[permList[0]];
456             return response;
457         });
458     }
459
460     return service;
461 }])
462
463