expire mfa tokens through Oban
authorAlexander Strizhakov <alex.strizhakov@gmail.com>
Sat, 5 Sep 2020 15:35:01 +0000 (18:35 +0300)
committerAlexander Strizhakov <alex.strizhakov@gmail.com>
Thu, 10 Sep 2020 13:01:19 +0000 (16:01 +0300)
config/config.exs
docs/configuration/cheatsheet.md
lib/pleroma/mfa/token.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/oauth/token.ex
lib/pleroma/web/oauth/token/clean_worker.ex [deleted file]
lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
lib/pleroma/workers/purge_expired_token.ex
test/web/twitter_api/remote_follow_controller_test.exs
test/workers/purge_expired_oauth_token_test.exs [deleted file]
test/workers/purge_expired_token_test.exs [new file with mode: 0644]

index fa4c96b79ddb43ac9a8b8cd29daaf162d07b2918..95a6ea9db59457d00f51482551b4eefa292c97bc 100644 (file)
@@ -530,7 +530,7 @@ config :pleroma, Oban,
   log: false,
   queues: [
     activity_expiration: 10,
-    oauth_token_expiration: 1,
+    token_expiration: 5,
     federator_incoming: 50,
     federator_outgoing: 50,
     web_push: 50,
index ec59896ecd16dba049606ae13d18c3ade799fd4f..d0bebbd4521e13c13683d441664d0a3ae0758265 100644 (file)
@@ -691,9 +691,8 @@ Pleroma has the following queues:
 
 Pleroma has these periodic job workers:
 
-`Pleroma.Workers.Cron.ClearOauthTokenWorker` - a job worker to cleanup expired oauth tokens.
-
-Example:
+* `Pleroma.Workers.Cron.DigestEmailsWorker` - digest emails for users with new mentions and follows
+* `Pleroma.Workers.Cron.NewUsersDigestWorker` - digest emails for admins with new registrations
 
 ```elixir
 config :pleroma, Oban,
@@ -705,7 +704,8 @@ config :pleroma, Oban,
     federator_outgoing: 50
   ],
   crontab: [
-    {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}
+    {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
+    {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
   ]
 ```
 
@@ -972,7 +972,7 @@ Configure OAuth 2 provider capabilities:
 
 * `token_expires_in` - The lifetime in seconds of the access token.
 * `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token.
-* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`. Interval settings sets in configuration periodic jobs [`Oban.Cron`](#obancron)
+* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`.
 
 ## Link parsing
 
index 0b2449971d9e47c5650dd09878b12ad0a9aefd6d..69b64c0e8ed73087a1912bbdf2312407382b2e91 100644 (file)
@@ -10,10 +10,11 @@ defmodule Pleroma.MFA.Token do
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.OAuth.Authorization
-  alias Pleroma.Web.OAuth.Token, as: OAuthToken
 
   @expires 300
 
+  @type t() :: %__MODULE__{}
+
   schema "mfa_tokens" do
     field(:token, :string)
     field(:valid_until, :naive_datetime_usec)
@@ -24,6 +25,7 @@ defmodule Pleroma.MFA.Token do
     timestamps()
   end
 
+  @spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
   def get_by_token(token) do
     from(
       t in __MODULE__,
@@ -33,33 +35,40 @@ defmodule Pleroma.MFA.Token do
     |> Repo.find_resource()
   end
 
-  def validate(token) do
-    with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
-         {:expired, false} <- {:expired, is_expired?(token)} do
+  @spec validate(String.t()) :: {:ok, t()} | {:error, :not_found} | {:error, :expired_token}
+  def validate(token_str) do
+    with {:ok, token} <- get_by_token(token_str),
+         false <- expired?(token) do
       {:ok, token}
-    else
-      {:expired, _} -> {:error, :expired_token}
-      {:fetch_token, _} -> {:error, :not_found}
-      error -> {:error, error}
     end
   end
 
-  def create_token(%User{} = user) do
-    %__MODULE__{}
-    |> change
-    |> assign_user(user)
-    |> put_token
-    |> put_valid_until
-    |> Repo.insert()
+  defp expired?(%__MODULE__{valid_until: valid_until}) do
+    with true <- NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 do
+      {:error, :expired_token}
+    end
+  end
+
+  @spec create(User.t(), Authorization.t() | nil) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
+  def create(user, authorization \\ nil) do
+    with {:ok, token} <- do_create(user, authorization) do
+      Pleroma.Workers.PurgeExpiredToken.enqueue(%{
+        token_id: token.id,
+        valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
+        mod: __MODULE__
+      })
+
+      {:ok, token}
+    end
   end
 
-  def create_token(user, authorization) do
+  defp do_create(user, authorization) do
     %__MODULE__{}
-    |> change
+    |> change()
     |> assign_user(user)
-    |> assign_authorization(authorization)
-    |> put_token
-    |> put_valid_until
+    |> maybe_assign_authorization(authorization)
+    |> put_token()
+    |> put_valid_until()
     |> Repo.insert()
   end
 
@@ -69,15 +78,19 @@ defmodule Pleroma.MFA.Token do
     |> validate_required([:user])
   end
 
-  defp assign_authorization(changeset, authorization) do
+  defp maybe_assign_authorization(changeset, %Authorization{} = authorization) do
     changeset
     |> put_assoc(:authorization, authorization)
     |> validate_required([:authorization])
   end
 
+  defp maybe_assign_authorization(changeset, _), do: changeset
+
   defp put_token(changeset) do
+    token = Pleroma.Web.OAuth.Token.Utils.generate_token()
+
     changeset
-    |> change(%{token: OAuthToken.Utils.generate_token()})
+    |> change(%{token: token})
     |> validate_required([:token])
     |> unique_constraint(:token)
   end
@@ -89,18 +102,4 @@ defmodule Pleroma.MFA.Token do
     |> change(%{valid_until: expires_in})
     |> validate_required([:valid_until])
   end
-
-  def is_expired?(%__MODULE__{valid_until: valid_until}) do
-    NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
-  end
-
-  def is_expired?(_), do: false
-
-  def delete_expired_tokens do
-    from(
-      q in __MODULE__,
-      where: fragment("?", q.valid_until) < ^Timex.now()
-    )
-    |> Repo.delete_all()
-  end
 end
index dd00600ea5ce3c7e67450e8706b8fdc0fb2f122f..bbe7aa8a014213c0c468365dd6843da073169c20 100644 (file)
@@ -197,7 +197,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
          {:mfa_required, user, auth, _},
          params
        ) do
-    {:ok, token} = MFA.Token.create_token(user, auth)
+    {:ok, token} = MFA.Token.create(user, auth)
 
     data = %{
       "mfa_token" => token.token,
@@ -579,7 +579,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     do: put_session(conn, :registration_id, registration_id)
 
   defp build_and_response_mfa_token(user, auth) do
-    with {:ok, token} <- MFA.Token.create_token(user, auth) do
+    with {:ok, token} <- MFA.Token.create(user, auth) do
       MFAView.render("mfa_response.json", %{token: token, user: user})
     end
   end
index 4d00fcb1cbe5b8762a9f4dc73165ba564945b2e5..de37998f24578b60f28aee2c441d31840629273a 100644 (file)
@@ -87,9 +87,10 @@ defmodule Pleroma.Web.OAuth.Token do
   def create(%App{} = app, %User{} = user, attrs \\ %{}) do
     with {:ok, token} <- do_create(app, user, attrs) do
       if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do
-        Pleroma.Workers.PurgeExpiredOAuthToken.enqueue(%{
+        Pleroma.Workers.PurgeExpiredToken.enqueue(%{
           token_id: token.id,
-          valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC")
+          valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
+          mod: __MODULE__
         })
       end
 
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
deleted file mode 100644 (file)
index 2f51bdb..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OAuth.Token.CleanWorker do
-  @moduledoc """
-  The module represents functions to clean an expired OAuth and MFA tokens.
-  """
-  use GenServer
-
-  @ten_seconds 10_000
-  @one_day 86_400_000
-
-  alias Pleroma.MFA
-  alias Pleroma.Workers.BackgroundWorker
-
-  def start_link(_), do: GenServer.start_link(__MODULE__, %{})
-
-  def init(_) do
-    Process.send_after(self(), :perform, @ten_seconds)
-    {:ok, nil}
-  end
-
-  @doc false
-  def handle_info(:perform, state) do
-    BackgroundWorker.enqueue("clean_expired_tokens", %{})
-    interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
-
-    Process.send_after(self(), :perform, interval)
-    {:noreply, state}
-  end
-
-  def perform(:clean) do
-    MFA.Token.delete_expired_tokens()
-  end
-end
index 521dc9322af5a059f0cd463e6aad1a9f937d9f2c..072d889e2e54e7a9b5bdd105ce95e6290490ae0a 100644 (file)
@@ -135,7 +135,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
   end
 
   defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
-    {:ok, %{token: token}} = MFA.Token.create_token(user)
+    {:ok, %{token: token}} = MFA.Token.create(user)
     render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
   end
 
index 6068e43bfbd6fc4dff9af20b0fb6e07546e6a2a1..a81e0cd28b1b88fe9793afd7a626baa8614e30eb 100644 (file)
@@ -2,14 +2,14 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Workers.PurgeExpiredOAuthToken do
+defmodule Pleroma.Workers.PurgeExpiredToken do
   @moduledoc """
   Worker which purges expired OAuth tokens
   """
 
-  use Oban.Worker, queue: :oauth_token_expiration, max_attempts: 1
+  use Oban.Worker, queue: :token_expiration, max_attempts: 1
 
-  @spec enqueue(%{token_id: integer(), valid_until: DateTime.t()}) ::
+  @spec enqueue(%{token_id: integer(), valid_until: DateTime.t(), mod: module()}) ::
           {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
   def enqueue(args) do
     {scheduled_at, args} = Map.pop(args, :valid_until)
@@ -20,8 +20,9 @@ defmodule Pleroma.Workers.PurgeExpiredOAuthToken do
   end
 
   @impl true
-  def perform(%Oban.Job{args: %{"token_id" => id}}) do
-    Pleroma.Web.OAuth.Token
+  def perform(%Oban.Job{args: %{"token_id" => id, "mod" => module}}) do
+    module
+    |> String.to_existing_atom()
     |> Pleroma.Repo.get(id)
     |> Pleroma.Repo.delete()
   end
index f7e54c26ae6ed5d2ce7ef54b11f34ca6d845a188..3852c7ce907f7080bebfe4006c81282db89e5599 100644 (file)
@@ -227,7 +227,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
           }
         )
 
-      {:ok, %{token: token}} = MFA.Token.create_token(user)
+      {:ok, %{token: token}} = MFA.Token.create(user)
 
       user2 = insert(:user)
       otp_token = TOTP.generate_token(otp_secret)
@@ -256,7 +256,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
           }
         )
 
-      {:ok, %{token: token}} = MFA.Token.create_token(user)
+      {:ok, %{token: token}} = MFA.Token.create(user)
 
       user2 = insert(:user)
       otp_token = TOTP.generate_token(TOTP.generate_secret())
diff --git a/test/workers/purge_expired_oauth_token_test.exs b/test/workers/purge_expired_oauth_token_test.exs
deleted file mode 100644 (file)
index 3bd650d..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Workers.PurgeExpiredOAuthTokenTest do
-  use Pleroma.DataCase, async: true
-  use Oban.Testing, repo: Pleroma.Repo
-
-  import Pleroma.Factory
-
-  setup do: clear_config([:oauth2, :clean_expired_tokens], true)
-
-  test "purges expired token" do
-    user = insert(:user)
-    app = insert(:oauth_app)
-
-    {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user)
-
-    assert_enqueued(
-      worker: Pleroma.Workers.PurgeExpiredOAuthToken,
-      args: %{token_id: id}
-    )
-
-    assert {:ok, %{id: ^id}} =
-             perform_job(Pleroma.Workers.PurgeExpiredOAuthToken, %{token_id: id})
-  end
-end
diff --git a/test/workers/purge_expired_token_test.exs b/test/workers/purge_expired_token_test.exs
new file mode 100644 (file)
index 0000000..fb7708c
--- /dev/null
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.PurgeExpiredTokenTest do
+  use Pleroma.DataCase, async: true
+  use Oban.Testing, repo: Pleroma.Repo
+
+  import Pleroma.Factory
+
+  setup do: clear_config([:oauth2, :clean_expired_tokens], true)
+
+  test "purges expired oauth token" do
+    user = insert(:user)
+    app = insert(:oauth_app)
+
+    {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user)
+
+    assert_enqueued(
+      worker: Pleroma.Workers.PurgeExpiredToken,
+      args: %{token_id: id, mod: Pleroma.Web.OAuth.Token}
+    )
+
+    assert {:ok, %{id: ^id}} =
+             perform_job(Pleroma.Workers.PurgeExpiredToken, %{
+               token_id: id,
+               mod: Pleroma.Web.OAuth.Token
+             })
+
+    assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0
+  end
+
+  test "purges expired mfa token" do
+    authorization = insert(:oauth_authorization)
+
+    {:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization)
+
+    assert_enqueued(
+      worker: Pleroma.Workers.PurgeExpiredToken,
+      args: %{token_id: id, mod: Pleroma.MFA.Token}
+    )
+
+    assert {:ok, %{id: ^id}} =
+             perform_job(Pleroma.Workers.PurgeExpiredToken, %{
+               token_id: id,
+               mod: Pleroma.MFA.Token
+             })
+
+    assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0
+  end
+end