Merge branch 'openapi/validation-plug' into 'develop'
Ignore unexpected query params and ENUM values Closes #1719 See merge request pleroma/pleroma!2468
This commit is contained in:
commit
5482a1f6ef
13 changed files with 173 additions and 7 deletions
|
@ -653,6 +653,8 @@ config :pleroma, :restrict_unauthenticated,
|
||||||
profiles: %{local: false, remote: false},
|
profiles: %{local: false, remote: false},
|
||||||
activities: %{local: false, remote: false}
|
activities: %{local: false, remote: false}
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{Mix.env()}.exs"
|
import_config "#{Mix.env()}.exs"
|
||||||
|
|
|
@ -3195,5 +3195,19 @@ config :pleroma, :config_description, [
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
group: :pleroma,
|
||||||
|
key: Pleroma.Web.ApiSpec.CastAndValidate,
|
||||||
|
type: :group,
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :strict,
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Enables strict input validation (useful in development, not recommended in production)",
|
||||||
|
suggestions: [false]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -52,6 +52,8 @@ config :pleroma, Pleroma.Repo,
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
|
||||||
|
|
||||||
if File.exists?("./config/dev.secret.exs") do
|
if File.exists?("./config/dev.secret.exs") do
|
||||||
import_config "dev.secret.exs"
|
import_config "dev.secret.exs"
|
||||||
else
|
else
|
||||||
|
|
|
@ -96,6 +96,8 @@ config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
|
||||||
|
|
||||||
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
|
||||||
|
|
||||||
if File.exists?("./config/test.secret.exs") do
|
if File.exists?("./config/test.secret.exs") do
|
||||||
import_config "test.secret.exs"
|
import_config "test.secret.exs"
|
||||||
else
|
else
|
||||||
|
|
|
@ -925,3 +925,7 @@ Restrict access for unauthenticated users to timelines (public and federate), us
|
||||||
* `activities` - statuses
|
* `activities` - statuses
|
||||||
* `local`
|
* `local`
|
||||||
* `remote`
|
* `remote`
|
||||||
|
|
||||||
|
## Pleroma.Web.ApiSpec.CastAndValidate
|
||||||
|
|
||||||
|
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
|
||||||
|
|
139
lib/pleroma/web/api_spec/cast_and_validate.ex
Normal file
139
lib/pleroma/web/api_spec/cast_and_validate.ex
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
|
||||||
|
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
||||||
|
@moduledoc """
|
||||||
|
This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
|
||||||
|
(https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
|
||||||
|
The main difference is ignoring unexpected query params instead of throwing
|
||||||
|
an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
|
||||||
|
to disable this behavior. Also, the default rendering error module
|
||||||
|
is `Pleroma.Web.ApiSpec.RenderError`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@behaviour Plug
|
||||||
|
|
||||||
|
alias Plug.Conn
|
||||||
|
|
||||||
|
@impl Plug
|
||||||
|
def init(opts) do
|
||||||
|
opts
|
||||||
|
|> Map.new()
|
||||||
|
|> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Plug
|
||||||
|
def call(%{private: %{open_api_spex: private_data}} = conn, %{
|
||||||
|
operation_id: operation_id,
|
||||||
|
render_error: render_error
|
||||||
|
}) do
|
||||||
|
spec = private_data.spec
|
||||||
|
operation = private_data.operation_lookup[operation_id]
|
||||||
|
|
||||||
|
content_type =
|
||||||
|
case Conn.get_req_header(conn, "content-type") do
|
||||||
|
[header_value | _] ->
|
||||||
|
header_value
|
||||||
|
|> String.split(";")
|
||||||
|
|> List.first()
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private_data = Map.put(private_data, :operation_id, operation_id)
|
||||||
|
conn = Conn.put_private(conn, :open_api_spex, private_data)
|
||||||
|
|
||||||
|
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
|
||||||
|
{:ok, conn} ->
|
||||||
|
conn
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
opts = render_error.init(reason)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> render_error.call(opts)
|
||||||
|
|> Plug.Conn.halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(
|
||||||
|
%{
|
||||||
|
private: %{
|
||||||
|
phoenix_controller: controller,
|
||||||
|
phoenix_action: action,
|
||||||
|
open_api_spex: private_data
|
||||||
|
}
|
||||||
|
} = conn,
|
||||||
|
opts
|
||||||
|
) do
|
||||||
|
operation =
|
||||||
|
case private_data.operation_lookup[{controller, action}] do
|
||||||
|
nil ->
|
||||||
|
operation_id = controller.open_api_operation(action).operationId
|
||||||
|
operation = private_data.operation_lookup[operation_id]
|
||||||
|
|
||||||
|
operation_lookup =
|
||||||
|
private_data.operation_lookup
|
||||||
|
|> Map.put({controller, action}, operation)
|
||||||
|
|
||||||
|
OpenApiSpex.Plug.Cache.adapter().put(
|
||||||
|
private_data.spec_module,
|
||||||
|
{private_data.spec, operation_lookup}
|
||||||
|
)
|
||||||
|
|
||||||
|
operation
|
||||||
|
|
||||||
|
operation ->
|
||||||
|
operation
|
||||||
|
end
|
||||||
|
|
||||||
|
if operation.operationId do
|
||||||
|
call(conn, Map.put(opts, :operation_id, operation.operationId))
|
||||||
|
else
|
||||||
|
raise "operationId was not found in action API spec"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
|
||||||
|
|
||||||
|
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
|
||||||
|
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
|
||||||
|
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
|
||||||
|
{:ok, conn} ->
|
||||||
|
{:ok, conn}
|
||||||
|
|
||||||
|
# Remove unexpected query params and cast/validate again
|
||||||
|
{:error, errors} ->
|
||||||
|
query_params =
|
||||||
|
Enum.reduce(errors, conn.query_params, fn
|
||||||
|
%{reason: :unexpected_field, name: name, path: [name]}, params ->
|
||||||
|
Map.delete(params, name)
|
||||||
|
|
||||||
|
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
|
||||||
|
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
|
||||||
|
update_in(params, path, &List.delete(&1, value))
|
||||||
|
|
||||||
|
_, params ->
|
||||||
|
params
|
||||||
|
end)
|
||||||
|
|
||||||
|
conn = %Conn{conn | query_params: query_params}
|
||||||
|
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_items_to_string(list) do
|
||||||
|
Enum.map(list, fn
|
||||||
|
i when is_atom(i) -> to_string(i)
|
||||||
|
i -> i
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
|
||||||
|
end
|
|
@ -17,6 +17,9 @@ defmodule Pleroma.Web.ApiSpec.RenderError do
|
||||||
def call(conn, errors) do
|
def call(conn, errors) do
|
||||||
errors =
|
errors =
|
||||||
Enum.map(errors, fn
|
Enum.map(errors, fn
|
||||||
|
%{name: nil, reason: :invalid_enum} = err ->
|
||||||
|
%OpenApiSpex.Cast.Error{err | name: err.value}
|
||||||
|
|
||||||
%{name: nil} = err ->
|
%{name: nil} = err ->
|
||||||
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
|
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
|
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
|
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
@local_mastodon_name "Mastodon-Local"
|
@local_mastodon_name "Mastodon-Local"
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
|
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
:skip_plug,
|
:skip_plug,
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
|
|
||||||
@oauth_read_actions [:show, :index]
|
@oauth_read_actions [:show, :index]
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
||||||
|
|
||||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
|
||||||
|
|
Loading…
Reference in a new issue