202 lines
11 KiB
JavaScript
202 lines
11 KiB
JavaScript
(function () {
|
|
angular.module('umbraco.security.retryQueue', []);
|
|
angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']);
|
|
angular.module('umbraco.security', [
|
|
'umbraco.security.retryQueue',
|
|
'umbraco.security.interceptor'
|
|
]);
|
|
//TODO: This is silly and unecessary to have a separate module for this
|
|
angular.module('umbraco.security.retryQueue', []) // This is a generic retry queue for security failures. Each item is expected to expose two functions: retry and cancel.
|
|
.factory('securityRetryQueue', [
|
|
'$q',
|
|
'$log',
|
|
function ($q, $log) {
|
|
var retryQueue = [];
|
|
var retryUser = null;
|
|
var service = {
|
|
// The security service puts its own handler in here!
|
|
onItemAddedCallbacks: [],
|
|
hasMore: function () {
|
|
return retryQueue.length > 0;
|
|
},
|
|
push: function (retryItem) {
|
|
retryQueue.push(retryItem);
|
|
// Call all the onItemAdded callbacks
|
|
angular.forEach(service.onItemAddedCallbacks, function (cb) {
|
|
try {
|
|
cb(retryItem);
|
|
} catch (e) {
|
|
$log.error('securityRetryQueue.push(retryItem): callback threw an error' + e);
|
|
}
|
|
});
|
|
},
|
|
pushRetryFn: function (reason, userName, retryFn) {
|
|
// The reason parameter is optional
|
|
if (arguments.length === 2) {
|
|
retryFn = userName;
|
|
userName = reason;
|
|
reason = undefined;
|
|
}
|
|
if (retryUser && retryUser !== userName || userName === null) {
|
|
throw new Error('invalid user');
|
|
}
|
|
retryUser = userName;
|
|
// The deferred object that will be resolved or rejected by calling retry or cancel
|
|
var deferred = $q.defer();
|
|
var retryItem = {
|
|
reason: reason,
|
|
retry: function () {
|
|
// Wrap the result of the retryFn into a promise if it is not already
|
|
$q.when(retryFn()).then(function (value) {
|
|
// If it was successful then resolve our deferred
|
|
deferred.resolve(value);
|
|
}, function (value) {
|
|
// Othewise reject it
|
|
deferred.reject(value);
|
|
});
|
|
},
|
|
cancel: function () {
|
|
// Give up on retrying and reject our deferred
|
|
deferred.reject();
|
|
}
|
|
};
|
|
service.push(retryItem);
|
|
return deferred.promise;
|
|
},
|
|
retryReason: function () {
|
|
return service.hasMore() && retryQueue[0].reason;
|
|
},
|
|
cancelAll: function () {
|
|
while (service.hasMore()) {
|
|
retryQueue.shift().cancel();
|
|
}
|
|
retryUser = null;
|
|
},
|
|
retryAll: function (userName) {
|
|
if (retryUser == null) {
|
|
return;
|
|
}
|
|
if (retryUser !== userName) {
|
|
service.cancelAll();
|
|
return;
|
|
}
|
|
while (service.hasMore()) {
|
|
retryQueue.shift().retry();
|
|
}
|
|
}
|
|
};
|
|
return service;
|
|
}
|
|
]);
|
|
angular.module('umbraco.security.interceptor') // This http interceptor listens for authentication successes and failures
|
|
.factory('securityInterceptor', [
|
|
'$injector',
|
|
'securityRetryQueue',
|
|
'notificationsService',
|
|
'eventsService',
|
|
'requestInterceptorFilter',
|
|
function ($injector, queue, notifications, eventsService, requestInterceptorFilter) {
|
|
return function (promise) {
|
|
return promise.then(function (originalResponse) {
|
|
// Intercept successful requests
|
|
//Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it
|
|
//expires. Then we'll update the user in the user service accordingly.
|
|
var headers = originalResponse.headers();
|
|
if (headers['x-umb-user-seconds']) {
|
|
// We must use $injector to get the $http service to prevent circular dependency
|
|
var userService = $injector.get('userService');
|
|
userService.setUserTimeout(headers['x-umb-user-seconds']);
|
|
}
|
|
//this checks if the user's values have changed, in which case we need to update the user details throughout
|
|
//the back office similar to how we do when a user logs in
|
|
if (headers['x-umb-user-modified']) {
|
|
eventsService.emit('app.userRefresh');
|
|
}
|
|
return promise;
|
|
}, function (originalResponse) {
|
|
// Intercept failed requests
|
|
// Make sure we have the configuration of the request (don't we always?)
|
|
var config = originalResponse.config ? originalResponse.config : {};
|
|
// Make sure we have an object for the headers of the request
|
|
var headers = config.headers ? config.headers : {};
|
|
//Here we'll check if we should ignore the error (either based on the original header set or the request configuration)
|
|
if (headers['x-umb-ignore-error'] === 'ignore' || config.umbIgnoreErrors === true || angular.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(originalResponse.status) !== -1) {
|
|
//exit/ignore
|
|
return promise;
|
|
}
|
|
var filtered = _.find(requestInterceptorFilter(), function (val) {
|
|
return config.url.indexOf(val) > 0;
|
|
});
|
|
if (filtered) {
|
|
return promise;
|
|
}
|
|
if (originalResponse.status === 401) {
|
|
//A 401 means that the user is not logged in
|
|
//avoid an infinite loop
|
|
var umbRequestHelper = $injector.get('umbRequestHelper');
|
|
var getCurrentUserPath = umbRequestHelper.getApiUrl('authenticationApiBaseUrl', 'GetCurrentUser');
|
|
if (!originalResponse.config.url.endsWith(getCurrentUserPath)) {
|
|
var userService = $injector.get('userService');
|
|
// see above
|
|
//Associate the user name with the retry to ensure we retry for the right user
|
|
promise = userService.getCurrentUser().then(function (user) {
|
|
var userName = user ? user.name : null;
|
|
//The request bounced because it was not authorized - add a new request to the retry queue
|
|
return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() {
|
|
// We must use $injector to get the $http service to prevent circular dependency
|
|
return $injector.get('$http')(originalResponse.config);
|
|
});
|
|
});
|
|
}
|
|
} else if (originalResponse.status === 404) {
|
|
//a 404 indicates that the request was not found - this could be due to a non existing url, or it could
|
|
//be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it
|
|
var errMsg = 'The URL returned a 404 (not found): <br/><i>' + originalResponse.config.url.split('?')[0] + '</i>';
|
|
if (originalResponse.data && originalResponse.data.ExceptionMessage) {
|
|
errMsg += '<br/> with error: <br/><i>' + originalResponse.data.ExceptionMessage + '</i>';
|
|
}
|
|
if (originalResponse.config.data) {
|
|
errMsg += '<br/> with data: <br/><i>' + angular.toJson(originalResponse.config.data) + '</i><br/>Contact your administrator for information.';
|
|
}
|
|
notifications.error('Request error', errMsg);
|
|
} else if (originalResponse.status === 403) {
|
|
//if the status was a 403 it means the user didn't have permission to do what the request was trying to do.
|
|
//How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was
|
|
//requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper,
|
|
// or completely custom for services calling resources.
|
|
//http://issues.umbraco.org/issue/U4-2749
|
|
//It was decided to just put these messages into the normal status messages.
|
|
var msg = 'Unauthorized access to URL: <br/><i>' + originalResponse.config.url.split('?')[0] + '</i>';
|
|
if (originalResponse.config.data) {
|
|
msg += '<br/> with data: <br/><i>' + angular.toJson(originalResponse.config.data) + '</i><br/>Contact your administrator for information.';
|
|
}
|
|
notifications.error('Authorization error', msg);
|
|
}
|
|
return promise;
|
|
});
|
|
};
|
|
}
|
|
]) //used to set headers on all requests where necessary
|
|
.factory('umbracoRequestInterceptor', function ($q, queryStrings) {
|
|
return {
|
|
//dealing with requests:
|
|
'request': function (config) {
|
|
if (queryStrings.getParams().umbDebug === 'true' || queryStrings.getParams().umbdebug === 'true') {
|
|
config.headers['X-UMB-DEBUG'] = 'true';
|
|
}
|
|
return config;
|
|
}
|
|
};
|
|
}).value('requestInterceptorFilter', function () {
|
|
return ['www.gravatar.com'];
|
|
}) // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block.
|
|
.config([
|
|
'$httpProvider',
|
|
function ($httpProvider) {
|
|
$httpProvider.defaults.xsrfHeaderName = 'X-UMB-XSRF-TOKEN';
|
|
$httpProvider.defaults.xsrfCookieName = 'UMB-XSRF-TOKEN';
|
|
$httpProvider.responseInterceptors.push('securityInterceptor');
|
|
$httpProvider.interceptors.push('umbracoRequestInterceptor');
|
|
}
|
|
]);
|
|
}()); |