Merge branch 'open-api' into 'develop'
Add OpenAPI Closes pleroma-meta#19 See merge request pleroma/pleroma!2345
This commit is contained in:
commit
d2e21fbc96
44
lib/pleroma/web/api_spec.ex
Normal file
44
lib/pleroma/web/api_spec.ex
Normal file
@ -0,0 +1,44 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec do
|
||||
alias OpenApiSpex.OpenApi
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
@behaviour OpenApi
|
||||
|
||||
@impl OpenApi
|
||||
def spec do
|
||||
%OpenApi{
|
||||
servers: [
|
||||
# Populate the Server info from a phoenix endpoint
|
||||
OpenApiSpex.Server.from_endpoint(Endpoint)
|
||||
],
|
||||
info: %OpenApiSpex.Info{
|
||||
title: "Pleroma",
|
||||
description: Application.spec(:pleroma, :description) |> to_string(),
|
||||
version: Application.spec(:pleroma, :vsn) |> to_string()
|
||||
},
|
||||
# populate the paths from a phoenix router
|
||||
paths: OpenApiSpex.Paths.from_router(Router),
|
||||
components: %OpenApiSpex.Components{
|
||||
securitySchemes: %{
|
||||
"oAuth" => %OpenApiSpex.SecurityScheme{
|
||||
type: "oauth2",
|
||||
flows: %OpenApiSpex.OAuthFlows{
|
||||
password: %OpenApiSpex.OAuthFlow{
|
||||
authorizationUrl: "/oauth/authorize",
|
||||
tokenUrl: "/oauth/token",
|
||||
scopes: %{"read" => "read"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# discover request/response schemas from path specs
|
||||
|> OpenApiSpex.resolve_schema_modules()
|
||||
end
|
||||
end
|
27
lib/pleroma/web/api_spec/helpers.ex
Normal file
27
lib/pleroma/web/api_spec/helpers.ex
Normal file
@ -0,0 +1,27 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Helpers do
|
||||
def request_body(description, schema_ref, opts \\ []) do
|
||||
media_types = ["application/json", "multipart/form-data"]
|
||||
|
||||
content =
|
||||
media_types
|
||||
|> Enum.map(fn type ->
|
||||
{type,
|
||||
%OpenApiSpex.MediaType{
|
||||
schema: schema_ref,
|
||||
example: opts[:example],
|
||||
examples: opts[:examples]
|
||||
}}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
%OpenApiSpex.RequestBody{
|
||||
description: description,
|
||||
content: content,
|
||||
required: opts[:required] || false
|
||||
}
|
||||
end
|
||||
end
|
96
lib/pleroma/web/api_spec/operations/app_operation.ex
Normal file
96
lib/pleroma/web/api_spec/operations/app_operation.ex
Normal file
@ -0,0 +1,96 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.AppOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
@spec create_operation() :: Operation.t()
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Create an application",
|
||||
description: "Create a new application to obtain OAuth2 credentials",
|
||||
operationId: "AppController.create",
|
||||
requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("App", "application/json", AppCreateResponse),
|
||||
422 =>
|
||||
Operation.response(
|
||||
"Unprocessable Entity",
|
||||
"application/json",
|
||||
%Schema{
|
||||
type: :object,
|
||||
description:
|
||||
"If a required parameter is missing or improperly formatted, the request will fail.",
|
||||
properties: %{
|
||||
error: %Schema{type: :string}
|
||||
},
|
||||
example: %{
|
||||
"error" => "Validation failed: Redirect URI must be an absolute URI."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def verify_credentials_operation do
|
||||
%Operation{
|
||||
tags: ["apps"],
|
||||
summary: "Verify your app works",
|
||||
description: "Confirm that the app's OAuth2 credentials work.",
|
||||
operationId: "AppController.verify_credentials",
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read"]
|
||||
}
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("App", "application/json", %Schema{
|
||||
type: :object,
|
||||
description:
|
||||
"If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
vapid_key: %Schema{type: :string},
|
||||
website: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"name" => "My App",
|
||||
"vapid_key" =>
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
}),
|
||||
422 =>
|
||||
Operation.response(
|
||||
"Unauthorized",
|
||||
"application/json",
|
||||
%Schema{
|
||||
type: :object,
|
||||
description:
|
||||
"If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.",
|
||||
properties: %{
|
||||
error: %Schema{type: :string}
|
||||
},
|
||||
example: %{
|
||||
"error" => "The access token is invalid."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
33
lib/pleroma/web/api_spec/schemas/app_create_request.ex
Normal file
33
lib/pleroma/web/api_spec/schemas/app_create_request.ex
Normal file
@ -0,0 +1,33 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
|
||||
alias OpenApiSpex.Schema
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AppCreateRequest",
|
||||
description: "POST body for creating an app",
|
||||
type: :object,
|
||||
properties: %{
|
||||
client_name: %Schema{type: :string, description: "A name for your application."},
|
||||
redirect_uris: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
|
||||
},
|
||||
scopes: %Schema{
|
||||
type: :string,
|
||||
description: "Space separated list of scopes. If none is provided, defaults to `read`."
|
||||
},
|
||||
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
|
||||
},
|
||||
required: [:client_name, :redirect_uris],
|
||||
example: %{
|
||||
"client_name" => "My App",
|
||||
"redirect_uris" => "https://myapp.com/auth/callback",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
})
|
||||
end
|
33
lib/pleroma/web/api_spec/schemas/app_create_response.ex
Normal file
33
lib/pleroma/web/api_spec/schemas/app_create_response.ex
Normal file
@ -0,0 +1,33 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AppCreateResponse",
|
||||
description: "Response schema for an app",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
client_id: %Schema{type: :string},
|
||||
client_secret: %Schema{type: :string},
|
||||
redirect_uri: %Schema{type: :string},
|
||||
vapid_key: %Schema{type: :string},
|
||||
website: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"id" => "123",
|
||||
"name" => "My App",
|
||||
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
|
||||
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
|
||||
"vapid_key" =>
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
|
||||
"website" => "https://myapp.com/"
|
||||
}
|
||||
})
|
||||
end
|
@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
|
||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
||||
|
||||
@local_mastodon_name "Mastodon-Local"
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
|
||||
|
||||
@doc "POST /api/v1/apps"
|
||||
def create(conn, params) do
|
||||
def create(%{body_params: params} = conn, _params) do
|
||||
scopes = Scopes.fetch_scopes(params, ["read"])
|
||||
|
||||
app_attrs =
|
||||
params
|
||||
|> Map.drop(["scope", "scopes"])
|
||||
|> Map.put("scopes", scopes)
|
||||
|> Map.take([:client_name, :redirect_uris, :website])
|
||||
|> Map.put(:scopes, scopes)
|
||||
|
||||
with cs <- App.register_changeset(%App{}, app_attrs),
|
||||
false <- cs.changes[:client_name] == @local_mastodon_name,
|
||||
|
@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do
|
||||
Note: `scopes` is used by Mastodon — supporting it but sticking to
|
||||
OAuth's standard `scope` wherever we control it
|
||||
"""
|
||||
@spec fetch_scopes(map(), list()) :: list()
|
||||
@spec fetch_scopes(map() | struct(), list()) :: list()
|
||||
|
||||
def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
|
||||
parse_scopes(scopes, default)
|
||||
end
|
||||
|
||||
def fetch_scopes(params, default) do
|
||||
parse_scopes(params["scope"] || params["scopes"], default)
|
||||
end
|
||||
|
@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do
|
||||
plug(Pleroma.Plugs.SetUserSessionIdPlug)
|
||||
plug(Pleroma.Plugs.EnsureUserKeyPlug)
|
||||
plug(Pleroma.Plugs.IdempotencyPlug)
|
||||
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
|
||||
end
|
||||
|
||||
pipeline :authenticated_api do
|
||||
@ -44,6 +45,7 @@ defmodule Pleroma.Web.Router do
|
||||
plug(Pleroma.Plugs.SetUserSessionIdPlug)
|
||||
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
|
||||
plug(Pleroma.Plugs.IdempotencyPlug)
|
||||
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
|
||||
end
|
||||
|
||||
pipeline :admin_api do
|
||||
@ -61,6 +63,7 @@ defmodule Pleroma.Web.Router do
|
||||
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
|
||||
plug(Pleroma.Plugs.UserIsAdminPlug)
|
||||
plug(Pleroma.Plugs.IdempotencyPlug)
|
||||
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
|
||||
end
|
||||
|
||||
pipeline :mastodon_html do
|
||||
@ -94,10 +97,12 @@ defmodule Pleroma.Web.Router do
|
||||
|
||||
pipeline :config do
|
||||
plug(:accepts, ["json", "xml"])
|
||||
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
|
||||
end
|
||||
|
||||
pipeline :pleroma_api do
|
||||
plug(:accepts, ["html", "json"])
|
||||
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
|
||||
end
|
||||
|
||||
pipeline :mailbox_preview do
|
||||
@ -500,6 +505,12 @@ defmodule Pleroma.Web.Router do
|
||||
)
|
||||
end
|
||||
|
||||
scope "/api" do
|
||||
pipe_through(:api)
|
||||
|
||||
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
|
||||
end
|
||||
|
||||
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
|
3
mix.exs
3
mix.exs
@ -179,7 +179,8 @@ defp deps do
|
||||
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
|
||||
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
|
||||
{:mox, "~> 0.5", only: :test},
|
||||
{:restarter, path: "./restarter"}
|
||||
{:restarter, path: "./restarter"},
|
||||
{:open_api_spex, "~> 3.6"}
|
||||
] ++ oauth_deps()
|
||||
end
|
||||
|
||||
|
1
mix.lock
1
mix.lock
@ -74,6 +74,7 @@
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
|
||||
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
|
||||
"oban": {:hex, :oban, "0.12.1", "695e9490c6e0edfca616d80639528e448bd29b3bff7b7dd10a56c79b00a5d7fb", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1d58d69b8b5a86e7167abbb8cc92764a66f25f12f6172052595067fc6a30a17"},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.6.0", "64205aba9f2607f71b08fd43e3351b9c5e9898ec5ef49fc0ae35890da502ade9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "126ba3473966277132079cb1d5bf1e3df9e36fe2acd00166e75fd125cecb59c5"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
|
||||
"phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
|
||||
|
45
test/web/api_spec/app_operation_test.exs
Normal file
45
test/web/api_spec/app_operation_test.exs
Normal file
@ -0,0 +1,45 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.AppOperationTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
|
||||
alias Pleroma.Web.ApiSpec
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
|
||||
|
||||
import OpenApiSpex.TestAssertions
|
||||
import Pleroma.Factory
|
||||
|
||||
test "AppCreateRequest example matches schema" do
|
||||
api_spec = ApiSpec.spec()
|
||||
schema = AppCreateRequest.schema()
|
||||
assert_schema(schema.example, "AppCreateRequest", api_spec)
|
||||
end
|
||||
|
||||
test "AppCreateResponse example matches schema" do
|
||||
api_spec = ApiSpec.spec()
|
||||
schema = AppCreateResponse.schema()
|
||||
assert_schema(schema.example, "AppCreateResponse", api_spec)
|
||||
end
|
||||
|
||||
test "AppController produces a AppCreateResponse", %{conn: conn} do
|
||||
api_spec = ApiSpec.spec()
|
||||
app_attrs = build(:oauth_app)
|
||||
|
||||
json =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post(
|
||||
"/api/v1/apps",
|
||||
Jason.encode!(%{
|
||||
client_name: app_attrs.client_name,
|
||||
redirect_uris: app_attrs.redirect_uris
|
||||
})
|
||||
)
|
||||
|> json_response(200)
|
||||
|
||||
assert_schema(json, "AppCreateResponse", api_spec)
|
||||
end
|
||||
end
|
@ -794,7 +794,9 @@ test "blocking / unblocking a user" do
|
||||
|
||||
test "Account registration via Application", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, "/api/v1/apps", %{
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/apps", %{
|
||||
client_name: "client_name",
|
||||
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
||||
scopes: "read, write, follow"
|
||||
|
@ -16,8 +16,7 @@ test "apps/verify_credentials", %{conn: conn} do
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, token.user)
|
||||
|> assign(:token, token)
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> get("/api/v1/apps/verify_credentials")
|
||||
|
||||
app = Repo.preload(token, :app).app
|
||||
@ -37,6 +36,7 @@ test "creates an oauth app", %{conn: conn} do
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> assign(:user, user)
|
||||
|> post("/api/v1/apps", %{
|
||||
client_name: app_attrs.client_name,
|
||||
|
Loading…
Reference in New Issue
Block a user