kubernetes / dashboard

General-purpose web UI for Kubernetes clusters
Apache License 2.0
14.52k stars 4.17k forks source link

Path probing fails in presence of redirects #9134

Open drigz opened 5 months ago

drigz commented 5 months ago

What happened?

We're hosting the dashboard behind an auth proxy on a subpath. However, we find that unless we create special ingress rules to work around this, the dashboard doesn't work in this environment:

The tab remains blank.

What did you expect to happen?

Probing requests that return 3xx status codes or HTML content are ignored, and the probing continues until it reaches the "real" config.json.

How can we reproduce it (as minimally and precisely as possible)?

I don't have a repro as it'd take some work to put one together with kind or similar rather than with our cloud infra - let me know if that would be decisive for whether this is addressed and/or whether a PR is accepted.

Anything else we need to know?

I was able to work around this by creating a new Ingress resource to serve errors on /assets/config/config.json for the probed, incorrect paths. Creating a new subdomain to expose the dashboard would have also worked.

What browsers are you seeing the problem on?

No response

Kubernetes Dashboard version

7.4.0

Kubernetes version

1.29.4

Dev environment

No response

floreks commented 5 months ago

We can extend the logic that tries to dynamically configure base href. It's quite annoying to configure an environment locally that will allow testing more complex scenarios like yours. If you've got an environment where you could test those changes, feel free to open a PR.

Palollo commented 4 months ago

I think I am in the same case... The problem is here:

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }

        const fullPath = origin + basePath + probePath;
        configFound = configExists(fullPath);
        if (configFound) {
          break;
        }
      }

As @drigz, I have the kubernetes dashboard in the path https://example.com/kubernetes-dashboard/, so the value of pathSegments is ["","kubernetes-dashboard",""].
The "for" loop is looking for config files in all the base paths.
When it tries with the first one "",
basePath = "/" + "" + "assets/config/config.json",
then configExist("https://example.com/assets/config/config.json") returns true because I have another application in that path which has another different "config.json" file! And the browser network log says it is corrupted (NS_ERROR_CORRUPTED_CONTENT), because it is not the expected config file.

Why it is looking for a config file in base paths?? It should look for the config file in the complete path: https://example.com/kubernetes-dashboard/assets/config/config.json
and not in the previous paths like this one: https://example.com/assets/config/config.json

It works when I change the previous code to that (taking the configExist() out of the loop, to try only the final complete path):

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }
      }
      const fullPath = origin + basePath + probePath;
      configFound = configExists(fullPath);
floreks commented 4 months ago

The reason it starts from the root and goes up is that if you i.e. use a direct link to a subview in Dashboard, it has to be able to find a base path it can use in combination with file subpath to get the config. It also doesn't know if it is being served on a root path or on a sub path thus starting from the root.

Consider these examples:

https://example.com - access main view
https://example.com/pods/xyz - access some pod details view

https://example.com/dashboard - access main view on a subpath
https://example.com/dashboard/pods/xyz - access some pod details view on a subpath

It could start from the full path and subtract parts of it, but unfortunately, it will be suboptimal. Long URLs will take longer to load initially as it will have to traverse through more paths to find the correct one.

Palollo commented 4 months ago

Thanks for your quick response @floreks, I don't know if I have different version than yours or different configuration, but in my case, the direct links are like this:

https://example.com/dashboard/ - access main view on a subpath - pathSegments is ["","dashboard",""]
https://example.com/dashboard/#/pod/mynamespace/xyz - access some pod details view on a subpath - pathSegments is the same ["","dashboard",""]

The path within the kubernetes-dashboard app is specified after the hash char, i.e. in the fragment part, not in the path part. So I think the solution you propose is good! Even if the URLs were like you say, the looking for the config file would be only in the initial access. And not so long, probably 2-3 levels, not more, I think. In my case 0 levels ;)

Palollo commented 4 months ago

It is better to have a solution working for all the cases, although suboptimal in some specific cases, than a solution optimal for some cases and not working on some other cases.

drigz commented 4 months ago

I have another application in that path which has another different "config.json" file!

Does your application serve valid JSON from /assets/config/config.json? What does the JSON look like, compared to the config from the application itself?

I think the "root cause" here is the false positive, and while reversing the search order trades speed against false-positive risk, another way to address it would be to make the check less likely to trigger falsely. I'd previously thought it would be enough to check for valid JSON that looks like dashboard config, but I'm now questioning whether there should be a purpose-specific /assets/sentinel.json that is unmistakeable in its content. (that said, I haven't had/taken the time to set up a dev environment and try this, so feel free to ignore my ideas in favor of those with time to work on this)

Palollo commented 3 months ago

Yes, that would be another solution in case of the search is required: start from the root path but check for a unique field value in the config file. As I said, in our case the search is not required and it's perfectly working.

This is the code we currently have:

      let url = document.location
      const configUrl = url + 'assets/config/config.json';
      configFound = configExists(configUrl);

      document.write("<base href='" + (configFound ? url : '') + "' />");

Instead of this:

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }

        const fullPath = origin + basePath + probePath;
        configFound = configExists(fullPath);
        if (configFound) {
          break;
        }
      }

      document.write("<base href='" + (configFound ? basePath : '') + "' />");
fmichea commented 2 months ago

@drigz When you originally wrote this issue, do you mind me asking to clarify if the tab did remain blank and prevent login or if you get something on the page ? Has this been fixed ? Is the behavior still the same today ?

The What did you expect to happen? section of your original comment and the following conversation makes it difficult to understand if you had false positives like @Palollo is describing or if you ran into a blank page even without those false positives. Did the correct sub-path get found in your case ?

On my end, I am seeing the same probing code go through the same steps, it does get a 404 for the first level, then finds the file under the correct path, so I think the code should be working as intended and finding the correct base path, but I still see a blank login page... for some time. Currently digging into that but want to confirm the behavior you observed.

In any case, I think it's fairly commons for web apps to allow specifying a sub-directory it is served under as an option. That would remove the use of probing code entirely and seems pretty straightforward. What do you think?


Here is how my case continues for the curious, I dont think it's 100% related :

floreks commented 2 months ago

Your case is different and looks to be an ingress misconfiguration. Did you disable kong and try to replace it with ingress?

drigz commented 2 months ago

When you originally wrote this issue, do you mind me asking to clarify if the tab did remain blank and prevent login or if you get something on the page ? Has this been fixed ? Is the behavior still the same today ?

The tab remained blank after a false positive (a 200 response with HTML in the response body) so I think it's the same thing Palollo described.

I haven't tried updating or removing my workaround since I filed this - I didn't see anything indicating that the probing behavior changed, and I didn't take the time to try to adjust it myself.

In any case, I think it's fairly commons for web apps to allow specifying a sub-directory it is served under as an option. That would remove the use of probing code entirely and seems pretty straightforward. What do you think?

I actually quite like the probing as we sometimes access the backend from a different path (when port-forwarding past our ingress directly to the dashboard backend). But I'd be OK with the fixed path.

fmichea commented 2 months ago

Cool thanks for confirming, as expected my issue was unrelated but I think this is good context. It's possible that having the option for a fixed prefix defined by configuration could be in addition to the probing code. Then the configured path would take precedence to probing and allow anyone who wants to bypass that piece of code without introducing a workaround. Just a thought, leaving you to decide on the next steps regarding this issue :) Thanks again

floreks commented 2 months ago

Unless something has changed in angular in the latest version that I am not aware of, there is no way to dynamically adjust app path during runtime via config. Path is part of the main HTML file and base url has to be adjusted there to work with the sub path. It can be only changed during the build or via JavaScript.

Palollo commented 1 month ago

Ok, my last attempt, if you want to fix now this issue, the conservative change I propose... Just check if config file exists (in the current location), and if not then do the search as you did, the optimal way. The "else" content is exactly the same previous code.

      let url = document.location
      const configUrl = url + 'assets/config/config.json';
      if (configExists(configUrl)) {  
        document.write("<base href='" + url + "' />");      
      }
      else {    // search for the config
        const probePath = 'assets/config/config.json';
        const origin = document.location.origin;
        const pathSegments = document.location.pathname.split('/');

        let basePath = '/';
        let configFound = false;

        for (let i = 0; i < pathSegments.length; i++) {
          const segment = pathSegments[i];
          if (segment.length > 0) {
            basePath = basePath + segment + '/';
          }

          const fullPath = origin + basePath + probePath;
          configFound = configExists(fullPath);
          if (configFound) {
            break;
          }
        }

        document.write("<base href='" + (configFound ? basePath : '') + "' />");
      }

Instead of this:

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }

        const fullPath = origin + basePath + probePath;
        configFound = configExists(fullPath);
        if (configFound) {
          break;
        }
      }

      document.write("<base href='" + (configFound ? basePath : '') + "' />");
BigFlagBurito commented 3 weeks ago

Hi, I have a different problem, but the same loop seems to be the cause of the problem.

Briefly summarised my configuration:

Due to the development of my web app, which can be accessed via /, an error occurred there. A request blocked the entire web app. This meant that other requests could no longer get through. When I wanted to find out why I could no longer access the website, I wanted to run a diagnosis via the dashboard. But for some inexplicable reason, I couldn't access the dashboard until the request to the web app timed out. After a very, very long search, I found the problem why I couldn't reach the dashboard. As it turned out, the dashboard makes a request to / for the assets.

As @fmichea suggested, it would be great if there could be an option to specify which sub-path the dashboard is accessible on.