summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames E. Blair <jim@acmegating.com>2022-09-22 09:34:25 -0700
committerJames E. Blair <jim@acmegating.com>2022-09-22 09:34:25 -0700
commit279547417fb406d0066c2f454ef93965f35a8754 (patch)
tree203390a878553e6f0fcae44642f17fdc0228e572
parentfd6af2931b3610fa80cd2d6d7ed6a035da27233d (diff)
downloadzuul-279547417fb406d0066c2f454ef93965f35a8754.tar.gz
Detect and handle auth proxy redirects
If the Zuul web UI is placed behind an authorizing proxy system, such as Apache mod_auth_mellon for SAML, then when the service provider token times out, users will not receive any indication that has occurred. If they are watching the status page, it will just silently fail to update. Switching to another page may bring it to their attention, unless we already have cached data, in which case there still may be no indication. In these cases, the authorizy proxy sends a redirect (HTTP 303) instead of the normal response, but since our requests are async background requests from JS, they are subject to CORS rules and because they arrive without an Access-Control-Allow-Origin header, the response is unavailable to us. (Even if they do arrive with that header (say by the use of apache mod_headers with "always set"), the ultimate target of the redirect also needs to have that header, which is very unlikely in the case of an ID provider.) Even though we have no information about the response due to the CORS restrictions, we can detect this situation, at least with mod_auth_mellon, and possibly others, by adding the X-Requested-With header. This can be used to indicate that the request is from JS instead of the user, and in this case, mod_auth_mellon returns a 403 instead of a 303. We do have access to the 403 response (unlike the 303) so we can detect this case. In other words after receiving an undefined response (which could be DNS, network, or CORS error), we can narrow that down by repeating the request with the X-Requested-With header, and if we then get a 403, we can be fairly certain the first error was CORS and that we need to re-authenticate. We still don't have access to the redirect target that the auth proxy wants us to use, so the best we can do is to just reload the page and let the auth proxy perform a redirect based on the normal user request that this appears as. Why not always include X-Requested-With? That's because without that header, the browser considers most of our GET requests to be "simple" which means that they do not require pre-flight checks (where the browser performs an OPTIONS request before executing the actual GET). Adding that header to every GET would double the number of HTTP requests in normal operation (even for sites without auth proxies), so it is worth our while to keep our simple GET requests simple. Change-Id: I7c82110d033550c451d21306de94f223a5fcceb2
-rw-r--r--web/src/api.js78
1 files changed, 56 insertions, 22 deletions
diff --git a/web/src/api.js b/web/src/api.js
index 1ba39998c..f9a3bd07d 100644
--- a/web/src/api.js
+++ b/web/src/api.js
@@ -103,93 +103,127 @@ function getStreamUrl(apiPrefix) {
return streamUrl
}
+function getWithCorsHandling(url) {
+ // This performs a simple GET and tries to detect if CORS errors are
+ // due to proxy authentication errors.
+ const instance = Axios.create({
+ baseURL: apiUrl
+ })
+ // First try the request as normal
+ let res = instance.get(url).catch(err => {
+ if (err.response === undefined) {
+ // This is either a Network, DNS, or CORS error, but we can't tell which.
+ // If we're behind an authz proxy, it's possible our creds have timed out
+ // and the CORS error is because we're getting a redirect.
+ // Apache mod_auth_mellon (and possibly other authz proxies) will avoid
+ // issuing a redirect if X-Requested-With is set to 'XMLHttpRequest' and
+ // will instead issue a 403. We can use this to detect that case.
+ instance.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
+ let res2 = instance.get(url).catch(err2 => {
+ if (err2.response && err2.response.status === 403) {
+ // We might be getting a redirect or something else,
+ // so reload the page.
+ console.log('Received 403 after unknown error; reloading')
+ window.location.reload()
+ }
+ // If we're still getting an error, we don't know the cause,
+ // it could be a transient network error, so we won't reload, we'll just
+ // wait for it to clear.
+ throw (err2)
+ })
+ return res2
+ }
+ })
+ return res
+}
+
// Direct APIs
function fetchInfo() {
- return Axios.get(apiUrl + 'info')
+ return getWithCorsHandling('info')
}
function fetchComponents() {
- return Axios.get(apiUrl + 'components')
+ return getWithCorsHandling('components')
}
function fetchTenantInfo(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'info')
+ return getWithCorsHandling(apiPrefix + 'info')
}
function fetchOpenApi() {
return Axios.get(getHomepageUrl() + 'openapi.yaml')
}
function fetchTenants() {
- return Axios.get(apiUrl + 'tenants')
+ return getWithCorsHandling(apiUrl + 'tenants')
}
function fetchConfigErrors(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'config-errors')
+ return getWithCorsHandling(apiPrefix + 'config-errors')
}
function fetchStatus(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'status')
+ return getWithCorsHandling(apiPrefix + 'status')
}
function fetchChangeStatus(apiPrefix, changeId) {
- return Axios.get(apiUrl + apiPrefix + 'status/change/' + changeId)
+ return getWithCorsHandling(apiPrefix + 'status/change/' + changeId)
}
function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) {
- return Axios.get(apiUrl + apiPrefix +
+ return getWithCorsHandling(apiPrefix +
'pipeline/' + pipelineName +
'/project/' + projectName +
'/branch/' + branchName +
'/freeze-job/' + jobName)
}
function fetchBuild(apiPrefix, buildId) {
- return Axios.get(apiUrl + apiPrefix + 'build/' + buildId)
+ return getWithCorsHandling(apiPrefix + 'build/' + buildId)
}
function fetchBuilds(apiPrefix, queryString) {
let path = 'builds'
if (queryString) {
path += '?' + queryString.slice(1)
}
- return Axios.get(apiUrl + apiPrefix + path)
+ return getWithCorsHandling(apiPrefix + path)
}
function fetchBuildset(apiPrefix, buildsetId) {
- return Axios.get(apiUrl + apiPrefix + 'buildset/' + buildsetId)
+ return getWithCorsHandling(apiPrefix + 'buildset/' + buildsetId)
}
function fetchBuildsets(apiPrefix, queryString) {
let path = 'buildsets'
if (queryString) {
path += '?' + queryString.slice(1)
}
- return Axios.get(apiUrl + apiPrefix + path)
+ return getWithCorsHandling(apiPrefix + path)
}
function fetchPipelines(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'pipelines')
+ return getWithCorsHandling(apiPrefix + 'pipelines')
}
function fetchProject(apiPrefix, projectName) {
- return Axios.get(apiUrl + apiPrefix + 'project/' + projectName)
+ return getWithCorsHandling(apiPrefix + 'project/' + projectName)
}
function fetchProjects(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'projects')
+ return getWithCorsHandling(apiPrefix + 'projects')
}
function fetchJob(apiPrefix, jobName) {
- return Axios.get(apiUrl + apiPrefix + 'job/' + jobName)
+ return getWithCorsHandling(apiPrefix + 'job/' + jobName)
}
function fetchJobGraph(apiPrefix, projectName, pipelineName, branchName) {
- return Axios.get(apiUrl + apiPrefix +
+ return getWithCorsHandling(apiPrefix +
'pipeline/' + pipelineName +
'/project/' + projectName +
'/branch/' + branchName +
'/freeze-jobs')
}
function fetchJobs(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'jobs')
+ return getWithCorsHandling(apiPrefix + 'jobs')
}
function fetchLabels(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'labels')
+ return getWithCorsHandling(apiPrefix + 'labels')
}
function fetchNodes(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'nodes')
+ return getWithCorsHandling(apiPrefix + 'nodes')
}
function fetchAutoholds(apiPrefix) {
- return Axios.get(apiUrl + apiPrefix + 'autohold')
+ return getWithCorsHandling(apiPrefix + 'autohold')
}
function fetchAutohold(apiPrefix, requestId) {
- return Axios.get(apiUrl + apiPrefix + 'autohold/' + requestId)
+ return getWithCorsHandling(apiPrefix + 'autohold/' + requestId)
}
// token-protected API