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();
66 var token = service.token();
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()});
78 // Otherwise, check the token. This will freeze all other interfaces, which is what we want.
81 'open-ils.auth.session.retrieve', token)
83 .then(function(user) {
84 if (user && user.classname) {
85 // authtoken test succeeded
88 service.check_workstation(deferred);
91 // authtoken test failed
92 egHatch.clearLoginSessionItems();
99 // no authtoken to test
100 deferred.reject('No authtoken found');
103 return deferred.promise;
106 service.check_workstation = function(deferred) {
108 var user = service.user();
109 var ws_path = '/admin/workstation/workstations';
111 return egHatch.getItem('eg.workstation.all')
112 .then(function(workstations) {
113 if (!workstations) workstations = [];
115 // If the user is authenticated with a workstation, get the
116 // name from the locally registered version of the workstation.
120 var ws = workstations.filter(
121 function(w) {return w.id == user.wsid()})[0];
124 service.ws = ws.name;
130 if ($location.path() == ws_path) {
131 // User is on the workstation admin page. No need
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.
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;
150 * Returns a promise, which is resolved on successful
151 * login and rejected on failed login.
153 service.login = function(args, ops) {
154 // avoid modifying the caller's data structure.
155 args = angular.copy(args);
157 if (!ops) { // only set on redo attempts.
158 ops = {deferred : $q.defer()};
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();
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
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
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();
192 return ops.deferred.promise;
196 * Returns a promise, which is resolved on successful
197 * login and rejected on failed login.
199 service.opChange = function(args) {
200 // avoid modifying the caller's data structure.
201 args = angular.copy(args);
202 args.workstation = service.workstation();
204 var deferred = $q.defer();
206 service.login_api(args).then(function(evt) {
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());
214 service.handle_login_ok(args, evt);
215 service.testAuthToken().then(
217 function () { service.opChangeUndo().then(deferred.reject) }
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));
228 return deferred.promise;
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');
239 return service.testAuthToken();
242 service.login_via_auth_proxy = function(args) {
243 return egNet.request(
244 'open-ils.auth_proxy',
245 'open-ils.auth_proxy.login', args);
248 service.login_via_auth = function(args) {
249 return egNet.request(
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));
258 return egNet.request(
260 'open-ils.auth.authenticate.complete', login_args)
265 service.login_api = function(args) {
267 return egNet.request(
268 'open-ils.auth_proxy',
269 'open-ils.auth_proxy.enabled')
272 console.log('proxy check returned ' + enabled);
273 if (Number(enabled) === 1) {
274 return service.login_via_auth_proxy(args);
276 return service.login_via_auth(args);
280 // request failed, likely a result of auth_proxy not running.
281 return service.login_via_auth(args);
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);
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?
302 service.poll = function() {
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});
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;
319 if (pollTime < 60000) {
320 // Never poll more often than once per minute.
322 } else if (pollTime > 2147483647) {
323 // Avoid integer overflow resulting in $timeout() effectively
324 // running with timeout=0 in a loop.
325 pollTime = 2147483647;
332 'open-ils.auth.session.retrieve',
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
340 // NOTE: we should never get here, since egNet
341 // filters responses for NO_SESSION events.
342 $rootScope.$broadcast('egAuthExpired');
350 service.logout = function(broadcast) {
352 if (broadcast && service.authChannel.postMessage) {
353 // Tell the other tabs to shut it all down.
354 service.authChannel.postMessage({action : 'logout'});
357 if (service.token()) {
360 'open-ils.auth.session.delete',
361 service.token()); // fire and forget
362 egHatch.clearLoginSessionItems();
364 service._user = null;
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
377 ['$q','egNet','egAuth','egOrg',
378 function($q , egNet , egAuth , egOrg) {
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.
388 service.hasPermAt = function(permList, asId) {
389 var deferred = $q.defer();
391 if (!angular.isArray(permList)) {
393 permList = [permList];
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.
400 'open-ils.actor.user.has_work_perm_at.batch',
401 egAuth.token(), permList
402 ).then(function(resp) {
404 angular.forEach(permList, function(perm) {
406 angular.forEach(resp[perm], function(oneOrg) {
407 all = all.concat(egOrg.descendants(oneOrg, asId));
411 if (!isArray) answer = answer[permList[0]];
412 deferred.resolve(answer);
414 return deferred.promise;
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.
423 service.hasPermHere = function(permList) {
427 if (!angular.isArray(permList)) {
429 permList = [permList];
432 // no workstation, all are false
433 if (egAuth.user().wsid() === null) {
434 console.warn("egPerm.hasPermHere() called with no workstation");
436 response = permList.map(function(perm) {
437 return response[perm] = false;
442 return $q.when(response);
445 ws_ou = Number(egAuth.user().ws_ou()); // from string
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
453 response[perm] = orgIds.indexOf(ws_ou) > -1;
455 if (!isArray) response = response[permList[0]];