diff options
author | James E. Blair <jim@acmegating.com> | 2022-09-22 09:34:25 -0700 |
---|---|---|
committer | James E. Blair <jim@acmegating.com> | 2022-09-22 09:34:25 -0700 |
commit | 279547417fb406d0066c2f454ef93965f35a8754 (patch) | |
tree | 203390a878553e6f0fcae44642f17fdc0228e572 /web/src | |
parent | fd6af2931b3610fa80cd2d6d7ed6a035da27233d (diff) | |
download | zuul-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
Diffstat (limited to 'web/src')
-rw-r--r-- | web/src/api.js | 78 |
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 |