Alternatively, I may be able to use this mechanism to write the actual response.
This is what I'm currently doing and it seems to work pretty well. It even means we don't need the option discussed above, as we can always perform a "live" permission check an arbitrarily long suffix of the pathInfo. (We start after the last AccessControlled).
DBS register themselves as soon as generateResponse is called (which seems safer than the constructor, as it's actual "time of use" rather than an object that could still be passed around), if we're not responding to a request on the second/resource domain, with…
- the full URL (except for the file path within the DBS),
- the nearest ancestor AccessControlled,
- the restOfPath,
- and the Authentication.name
…stored in a ResourceHolder wrapper object in a per-session map keyed by the URL (again with any file path within the DBS removed). If an equivalent ResourceHolder already exists for the URL key – and we compare object identity of the AccessControlled, it's reused, else a new one is added. Then whatever we got from that is added, if necessary, to a global list from UUID to ResourceHolder (WeakReference to that, actually, so hopefully reaping the sessions will remove obsolete {{ResourceHolder}}s but so far untested) which is what allows the request routing to work.
If a request arrives at the UnprotectedRootAction, we look up the ResourceHolder corresponding to the UUID, map the actually requested URL (with file path) to the corresponding restOfPath + filePath , and call Stapler#invoke(req, rsp, accessControlled, restOfPathPlusFilePath) as Authorization.name. That just writes the file into the response. The underlying assumption here is that the AccessControlled will implement a StaplerProxy style permission check, or the restOfPath contains enough permission checks – for a job workspace, we'd start routing at the job, go through its getTarget permission check, call doWs (with a more specific permission check) and let that handle the response.
To make sure requests go to the UnprotectedRootAction, a DBS holds an identifier (the UUID but can be anything) after successful registration. If it comes to serving a single file, the second/resource domain is configured, and we're not on the second/resource domain, we serve an HTTP 302 redirect to the corresponding URL over there. Otherwise, we serve the file directly, with CSP headers.
A few things left to figure out:
- The global list grows unbounded with no cleanup. It maps UUID to WeakReference<ResourceHolder – unsure how much of a problem that is.
- The per-session lists are built the same way (AFAICT) as BoundObjectTable with strong references in a session attribute, but I haven't seen this get cleaned up yet. Time to #doSimulateOutOfMemory and see what happens…
- Weird URLs like last*Build generate unnecessarily many instances, as we only look at the URL. Not sure I care enough to try to fix this.
- We apparently may end up holding references to obsolete objects after "Reload Configuration" is called.
- There may be problems around the renaming of projects, but these may actually be less than on "normal" URLs, as the URL only matters during registration, i.e. when the user accesses the DBS through a regular (non-resource) URL.
- Can we really rely on the assumption around the permission checks for the nearest ancestor, and if not, do we care enough? Do we need a guarantee around the expiration of URLs here to limit potential problems?
Now when you say second domain, can you clarify on the expected scope here? Here are some potential scope options:
Another orthogonal concern: using subdomains of the same domain versus completely separate domains (though since many static assets require authorization, the usual benefits of splitting up your CDN domain name from your app domain name don't apply; we still need the cookies).