@@ -0,0 +1,30 @@ | |||
# 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) | |||
} | |||
# discover request/response schemas from path specs | |||
|> OpenApiSpex.resolve_schema_modules() | |||
end | |||
end |
@@ -0,0 +1,94 @@ | |||
# 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.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: | |||
Operation.request_body("Parameters", "application/json", 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", | |||
parameters: [ | |||
Operation.parameter(:authorization, :header, :string, "Bearer <app token>", required: true) | |||
], | |||
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 |
@@ -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 |
@@ -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) | |||
@@ -171,7 +171,8 @@ defmodule Pleroma.Mixfile 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 | |||
@@ -72,6 +72,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"}, | |||
@@ -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 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest 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 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest 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 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do | |||
conn = | |||
conn | |||
|> put_req_header("content-type", "application/json") | |||
|> assign(:user, user) | |||
|> post("/api/v1/apps", %{ | |||
client_name: app_attrs.client_name, | |||