1 /* Core Sevice - egAuth
3 * Manages login and auth session retrieval.
6 angular.module('egCoreMod')
9 ['$q','$timeout','$rootScope','$window','$location','egNet','egHatch',
10 function($q , $timeout , $rootScope , $window , $location , egNet , egHatch) {
13 // the currently active user (au) object
21 // the user hidden by an operator change
22 OCuser : function(u) {
29 // the Op Change hidden auth token string
30 OCtoken : function() {
31 return egHatch.getLoginSessionItem('eg.auth.token.oc');
34 // Op Change hidden authtime in seconds
35 OCauthtime : function() {
36 return egHatch.getLoginSessionItem('eg.auth.time.oc');
39 // the currently active auth token string
41 return egHatch.getLoginSessionItem('eg.auth.token');
44 // authtime in seconds
45 authtime : function() {
46 return egHatch.getLoginSessionItem('eg.auth.time');
49 // the currently active workstation name
50 // For ws_ou or wsid(), see egAuth.user().ws_ou(), etc.
51 workstation : function() {
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')
62 /* Returns a promise, which is resolved if valid
63 * authtoken is found, otherwise rejected */
64 service.testAuthToken = function() {
65 var deferred = $q.defer();
67 // Move legacy cookies from /eg/staff to / before fetching the token.
68 egHatch.migrateAuthCookies();
70 var token = service.token();
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()});
82 // Otherwise, check the token. This will freeze all other interfaces, which is what we want.
85 'open-ils.auth.session.retrieve', token)
87 .then(function(user) {
88 if (user && user.classname) {
89 // authtoken test succeeded
92 service.check_workstation(deferred);
95 // authtoken test failed
96 egHatch.clearLoginSessionItems();
103 // no authtoken to test
104 deferred.reject('No authtoken found');
107 return deferred.promise;
110 service.check_workstation = function(deferred) {
112 var user = service.user();
113 var ws_path = '/admin/workstation/workstations';
115 return egHatch.getItem('eg.workstation.all')
116 .then(function(workstations) {
117 if (!workstations) workstations = [];
119 // If the user is authenticated with a workstation, get the
120 // name from the locally registered version of the workstation.
124 var ws = workstations.filter(
125 function(w) {return w.id == user.wsid()})[0];
128 service.ws = ws.name;
134 if ($location.path() == ws_path) {
135 // User is on the workstation admin page. No need
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.
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;
154 * Returns a promise, which is resolved on successful
155 * login and rejected on failed login.
157 service.login = function(args, ops) {
158 // avoid modifying the caller's data structure.
159 args = angular.copy(args);
161 if (!ops) { // only set on redo attempts.
162 ops = {deferred : $q.defer()};
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();
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
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
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();
196 return ops.deferred.promise;
200 * Returns a promise, which is resolved on successful
201 * login and rejected on failed login.
203 service.opChange = function(args) {
204 // avoid modifying the caller's data structure.
205 args = angular.copy(args);
206 args.workstation = service.workstation();
208 var deferred = $q.defer();
210 service.login_api(args).then(function(evt) {
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());
218 service.handle_login_ok(args, evt);
219 service.testAuthToken().then(
221 function () { service.opChangeUndo().then(deferred.reject) }
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));
232 return deferred.promise;
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');
243 return service.testAuthToken();
246 service.login_via_auth_proxy = function(args) {
247 return egNet.request(
248 'open-ils.auth_proxy',
249 'open-ils.auth_proxy.login', args);
252 service.login_via_auth = function(args) {
253 return egNet.request(
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));
262 return egNet.request(
264 'open-ils.auth.authenticate.complete', login_args)
269 service.login_api = function(args) {
271 return egNet.request(
272 'open-ils.auth_proxy',
273 'open-ils.auth_proxy.enabled')
276 console.log('proxy check returned ' + enabled);
277 if (Number(enabled) === 1) {
278 return service.login_via_auth_proxy(args);
280 return service.login_via_auth(args);
284 // request failed, likely a result of auth_proxy not running.
285 return service.login_via_auth(args);
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);
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?
306 service.poll = function() {
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});
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;
323 if (pollTime < 60000) {
324 // Never poll more often than once per minute.
326 } else if (pollTime > 2147483647) {
327 // Avoid integer overflow resulting in $timeout() effectively
328 // running with timeout=0 in a loop.
329 pollTime = 2147483647;
336 'open-ils.auth.session.retrieve',
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
344 // NOTE: we should never get here, since egNet
345 // filters responses for NO_SESSION events.
346 $rootScope.$broadcast('egAuthExpired');
354 service.logout = function(broadcast) {
356 if (broadcast && service.authChannel.postMessage) {
357 // Tell the other tabs to shut it all down.
358 service.authChannel.postMessage({action : 'logout'});
361 if (service.token()) {
364 'open-ils.auth.session.delete',
365 service.token()); // fire and forget
366 egHatch.clearLoginSessionItems();
368 service._user = null;
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
381 ['$q','egNet','egAuth','egOrg',
382 function($q , egNet , egAuth , egOrg) {
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.
392 service.hasPermAt = function(permList, asId) {
393 var deferred = $q.defer();
395 if (!angular.isArray(permList)) {
397 permList = [permList];
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.
404 'open-ils.actor.user.has_work_perm_at.batch',
405 egAuth.token(), permList
406 ).then(function(resp) {
408 angular.forEach(permList, function(perm) {
410 angular.forEach(resp[perm], function(oneOrg) {
411 all = all.concat(egOrg.descendants(oneOrg, asId));
415 if (!isArray) answer = answer[permList[0]];
416 deferred.resolve(answer);
418 return deferred.promise;
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.
427 service.hasPermHere = function(permList) {
431 if (!angular.isArray(permList)) {
433 permList = [permList];
436 // no workstation, all are false
437 if (egAuth.user().wsid() === null) {
438 console.warn("egPerm.hasPermHere() called with no workstation");
440 response = permList.map(function(perm) {
441 return response[perm] = false;
446 return $q.when(response);
449 ws_ou = Number(egAuth.user().ws_ou()); // from string
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
457 response[perm] = orgIds.indexOf(ws_ou) > -1;
459 if (!isArray) response = response[permList[0]];