we should probably use ||
[akkoma] / lib / pleroma / web / o_auth / o_auth_controller.ex
index 6a8006d31d44e3be878cb8b4c1a2ac1cf0433940..277df1c46683932b08b9669cfe97f7b34a6642a4 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.OAuth.OAuthController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Helpers.AuthHelper
   alias Pleroma.Helpers.UriHelper
   alias Pleroma.Maps
   alias Pleroma.MFA
@@ -58,75 +59,72 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   # after user already authorized to MastodonFE.
   # So we have to check client and token.
   def authorize(
-        %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
+        %Plug.Conn{assigns: %{token: %Token{} = token, user: %User{} = user}} = conn,
         %{"client_id" => client_id} = params
       ) do
     with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
          ^client_id <- t.app.client_id do
       handle_existing_authorization(conn, params)
     else
-      _ -> do_authorize(conn, params)
+      _ ->
+        maybe_reuse_token(conn, params, user.id)
     end
   end
 
-  def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
-
-  defp maybe_remove_token(%Plug.Conn{assigns: %{token: %{app: id}}} = conn, %App{id: id}) do
-    conn
+  def authorize(%Plug.Conn{} = conn, params) do
+    # if we have a user in the session, attempt to authenticate as them
+    # otherwise show the login form
+    maybe_reuse_token(conn, params, AuthHelper.get_session_user(conn))
   end
 
-  defp maybe_remove_token(conn, _app) do
-    conn
-    |> assign(:token, nil)
+  defp maybe_reuse_token(conn, params, user_id) when is_binary(user_id) do
+    with %User{} = user <- User.get_cached_by_id(user_id),
+         %App{} = app <- Repo.get_by(App, client_id: params["client_id"]),
+         {:ok, %Token{} = token} <- Token.get_preeexisting_by_app_and_user(app, user),
+         {:ok, %Authorization{} = auth} <-
+           Authorization.get_preeexisting_by_app_and_user(app, user) do
+      conn
+      |> assign(:token, token)
+      |> after_create_authorization(auth, %{"authorization" => params})
+    else
+      _ -> do_authorize(conn, params)
+    end
   end
 
+  defp maybe_reuse_token(conn, params, _user), do: do_authorize(conn, params)
+
   defp do_authorize(%Plug.Conn{} = conn, params) do
     app = Repo.get_by(App, client_id: params["client_id"])
-    conn = maybe_remove_token(conn, app)
     available_scopes = (app && app.scopes) || []
     scopes = Scopes.fetch_scopes(params, available_scopes)
 
-    # if we already have a token for this specific setup, we can use that
-    with false <- Params.truthy_param?(params["force_login"]),
-         %App{} <- app,
-         %{assigns: %{user: %Pleroma.User{} = user}} <- conn,
-         {:ok, %Token{} = token} <- Token.get_preexisting_by_app_and_user(app, user),
-         true <- scopes == token.scopes do
-      token = Repo.preload(token, :app)
+    user =
+      with %{assigns: %{user: %User{} = user}} <- conn do
+        user
+      else
+        _ -> nil
+      end
 
-      conn
-      |> assign(:token, token)
-      |> handle_existing_authorization(params)
-    else
-      _ ->
-        user =
-          with %{assigns: %{user: %User{} = user}} <- conn do
-            user
-          else
-            _ -> nil
-          end
-
-        scopes =
-          if scopes == [] do
-            available_scopes
-          else
-            scopes
-          end
-
-        # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
-        render(conn, Authenticator.auth_template(), %{
-          user: user,
-          app: app && Map.delete(app, :client_secret),
-          response_type: params["response_type"],
-          client_id: params["client_id"],
-          available_scopes: available_scopes,
-          scopes: scopes,
-          redirect_uri: params["redirect_uri"],
-          state: params["state"],
-          params: params,
-          view_module: OAuthView
-        })
-    end
+    scopes =
+      if scopes == [] do
+        available_scopes
+      else
+        scopes
+      end
+
+    # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
+    render(conn, Authenticator.auth_template(), %{
+      user: user,
+      app: app && Map.delete(app, :client_secret),
+      response_type: params["response_type"],
+      client_id: params["client_id"],
+      available_scopes: available_scopes,
+      scopes: scopes,
+      redirect_uri: params["redirect_uri"],
+      state: params["state"],
+      params: params,
+      view_module: OAuthView
+    })
   end
 
   defp handle_existing_authorization(
@@ -171,7 +169,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
     with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
          {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
-      after_create_authorization(conn, auth, params)
+      conn
+      |> AuthHelper.put_session_user(user.id)
+      |> after_create_authorization(auth, params)
     else
       error ->
         handle_create_authorization_error(conn, error, params)
@@ -211,11 +211,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
          {:error, scopes_issue},
          %{"authorization" => _} = params
        )
-       when scopes_issue in [:unsupported_scopes, :missing_scopes] do
+       when scopes_issue in [:unsupported_scopes, :missing_scopes, :user_is_not_an_admin] do
     # Per https://github.com/tootsuite/mastodon/blob/
     #   51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
     conn
-    |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
+    |> put_flash(:error, dgettext("errors", "This action is outside of authorized scopes"))
     |> put_status(:unauthorized)
     |> authorize(params)
   end
@@ -292,7 +292,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
          fixed_token = Token.Utils.fix_padding(params["code"]),
          {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
          %User{} = user <- User.get_cached_by_id(auth.user_id),
-         {:ok, token} <- Token.exchange_token(app, auth) do
+         {:ok, token} <- Token.get_or_exchange_token(auth, app, user) do
       after_token_exchange(conn, %{user: user, token: token})
     else
       error ->
@@ -341,8 +341,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   # Bad request
   def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
 
-  def after_token_exchange(%Plug.Conn{} = conn, %{token: _token} = view_params) do
+  def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
     conn
+    |> AuthHelper.put_session_token(token.token)
+    |> AuthHelper.put_session_user(token.user_id)
     |> json(OAuthView.render("token.json", view_params))
   end
 
@@ -401,7 +403,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do
 
   def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
     with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
-         {:ok, _oauth_token} <- RevokeToken.revoke(oauth_token) do
+         {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
+      conn =
+        with session_token = AuthHelper.get_session_token(conn),
+             %Token{token: ^session_token} <- oauth_token do
+          AuthHelper.delete_session_token(conn)
+        else
+          _ -> conn
+        end
+
       json(conn, %{})
     else
       _error ->
@@ -548,10 +558,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     else
       {:error, changeset} ->
         message =
-          Enum.map(changeset.errors, fn {field, {error, _}} ->
+          Enum.map_join(changeset.errors, "; ", fn {field, {error, _}} ->
             "#{field} #{error}"
           end)
-          |> Enum.join("; ")
 
         message =
           String.replace(
@@ -596,7 +605,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
        when is_list(requested_scopes) do
     with {:account_status, :active} <- {:account_status, User.account_status(user)},
-         {:ok, scopes} <- validate_scopes(app, requested_scopes),
+         requested_scopes <- Scopes.filter_admin_scopes(requested_scopes, user),
+         {:ok, scopes} <- validate_scopes(user, app, requested_scopes),
          {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
       {:ok, auth}
     end
@@ -628,15 +638,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     end
   end
 
-  @spec validate_scopes(App.t(), map() | list()) ::
+  @spec validate_scopes(User.t(), App.t(), map() | list()) ::
           {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
-  defp validate_scopes(%App{} = app, params) when is_map(params) do
+  defp validate_scopes(%User{} = user, %App{} = app, params) when is_map(params) do
     requested_scopes = Scopes.fetch_scopes(params, app.scopes)
-    validate_scopes(app, requested_scopes)
+    validate_scopes(user, app, requested_scopes)
   end
 
-  defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
-    Scopes.validate(requested_scopes, app.scopes)
+  defp validate_scopes(%User{} = user, %App{} = app, requested_scopes)
+       when is_list(requested_scopes) do
+    Scopes.validate(requested_scopes, app.scopes, user)
   end
 
   def default_redirect_uri(%App{} = app) do