open-api-spex / open_api_spex

Open API Specifications for Elixir Plug applications
Mozilla Public License 2.0
705 stars 183 forks source link

Evaluation of swaggerui plug configuration at runtime #492

Open D4no0 opened 2 years ago

D4no0 commented 2 years ago

I am using the on_complete option provided by swaggerui plug to preauthorize both basic auth and api key. While I can do this with the basic auth:

function() {
          ui.preauthorizeBasic("basicAuth", "test@mail.com", "123123");
}

I cannot do the same with the api key, since the tokens are generated and tracked in database, so I have to use the database to request the api key, however this is not possible because the plug options are evaluated at compile-time.

I also noticed the option config_url that can be passed for swaggerui to fetch configuration from an url. The problem here however is the same, because the plug options are defined in router, there is no way for me to generate a url that I will be able to serve from this server with dynamic configuration.

Is there any way around this?

mbuhot commented 2 years ago

You might be able to put an identifier in the session, then read it back in a controller that serves config_url to supply the API key at runtime?

Alternatively, you can wrap the SwaggerUI plug in your own plug that delays the init call to runtime. Something like:

defmodule YourAppWeb.Plug.SwaggerUIWrapper do
  @behaviour Plug

  def init(opts), do: opts
  def call(conn, opts) do
    api_key = get_api_key(conn)
    opts = Keyword.put(opts, :on_complete, "function() { ui.preauthorizeApiKey('api_key', '#{api_key}') }")
    opts = OpenApiSpex.Plug.SwaggerUI.init(opts)
    OpenApiSpex.Plug.SwaggerUI.call(conn, opts)
  end
end

If you're open to creating a PR to improve OpenApiSpex in this area, one simple way might be to include values from a Conn.assigns or Conn.private field as well as the compile-time config.

That way each app can create a SwaggerUIRuntimeConfig plug to perform any custom logic, and the results will be included in the rendered UI.

Something similar to Absinthe.Plug.assign_context

D4no0 commented 2 years ago

Yes the wrapper plug could work nicely as a workaround.

As for PR, I think that making a plug wrapper is not explicit enough (you lose the possibility to see the configuration inside of router). I was thinking more in the direction of adding the config as a function, that will be evaluated when the plug is called.

Something like:

on_complete: fn -> "function() { ui.preauthorizeApiKey('api_key', '#{api_key}') }" end

This should be possible without any drastic changes to swaggerui plug, since the UI is built in call function, and this also will allow for dynamic configuration per request basis, the thing that a plug wrapper could not achieve.

What do you think about this?

mbuhot commented 2 years ago

this also will allow for dynamic configuration per request basis, the thing that a plug wrapper could not achieve.

Can you expand on that? A plug that runs prior to SwaggerUI vs a callback invoked from the SwaggerUI plug seem equivalent to me.

Callbacks could certainly work, although I don't think anonymous functions are supported, eg in the CORS plug origin config (https://github.com/mschae/cors_plug/tree/v3.0.3#using-a-function0-or-function1-that-returns-the-allowed-origin-as-a-string)

For an app requiring complex runtime configuration of SwaggerUI, I'd consider using a Plug.Builder along with a hypothetical OpenApiSpex.Plug.SwaggerUI.put_config/2 function to set the config on the Conn.

defmodule MyAppWeb.SwaggerUI do
  use Plug.Builder

  plug :configure_swagger_ui
  plug OpenApiSpex.Plug.SwaggerUI

  def configure_swagger_ui(conn, opts) do
    config = [
      path: System.get_env("OPEN_API_PATH", "/api/openapi"),
      default_model_expand_depth: 3,
      display_operation_id: true,
      on_complete: on_complete(conn)
    ]

    OpenApiSpex.Plug.SwaggerUI.put_config(conn, config)
  end

  defp on_complete(conn) do
    api_key = MyApp.Auth.get_api_key(conn.assigns.current_user)

    """
    function() { ui.preauthorizeApiKey('api_key', '#{api_key}') }
    """
  end
end

The router would just route to the custom plug:

  get "/swaggerui", MyAppWeb.SwaggerUI
D4no0 commented 2 years ago

Can you expand on that? A plug that runs prior to SwaggerUI vs a callback invoked from the SwaggerUI plug seem equivalent to me.

Yes my bad, I thought that the swaggerui plug was initialized inside of init/1 in your example.

Callbacks could certainly work, although I don't think anonymous functions are supported, eg in the CORS plug origin config (https://github.com/mschae/cors_plug/tree/v3.0.3#using-a-function0-or-function1-that-returns-the-allowed-origin-as-a-string)

Wow, quite an interesting limitation, I never knew about this.

If you are OK with possibility to send functions as configuration parameters, I could make a PR to allow this.

thbar commented 1 year ago

Runtime configuration can be especially useful today to e.g. disable highlighting on a per-operation basis.

See:

Currently applicable via:

    get("/swaggerui", SwaggerUI,
      path: "/api/openapi",
      syntax_highlight: false
    )