diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pleroma/web/api_spec.ex | 44 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/helpers.ex | 27 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/operations/app_operation.ex | 96 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/schemas/app_create_request.ex | 33 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/schemas/app_create_response.ex | 33 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/controllers/app_controller.ex | 9 | ||||
| -rw-r--r-- | lib/pleroma/web/oauth/scopes.ex | 7 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 11 | 
8 files changed, 256 insertions, 4 deletions
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex new file mode 100644 index 000000000..41e48a085 --- /dev/null +++ b/lib/pleroma/web/api_spec.ex @@ -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 diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex new file mode 100644 index 000000000..35cf4c0d8 --- /dev/null +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -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 diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex new file mode 100644 index 000000000..26d8dbd42 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/app_operation.ex @@ -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 diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex new file mode 100644 index 000000000..8a83abef3 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_request.ex @@ -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 diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex new file mode 100644 index 000000000..f290fb031 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/app_create_response.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 5e2871f18..005c60444 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -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, diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 8ecf901f3..1023f16d4 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 5a0902739..3ecd59cd1 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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)  | 
