From f7146583e5f1c2d0e8a198db00dfafced79d0706 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 08:15:57 -0500
Subject: [PATCH 01/13] Remove LDAP mail attribute as a requirement for
 registering an account

---
 lib/pleroma/web/auth/ldap_authenticator.ex | 30 ++++++++--------------
 test/web/oauth/ldap_authorization_test.exs |  4 +--
 2 files changed, 12 insertions(+), 22 deletions(-)

diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index f63a66c03..f320ec746 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -105,29 +105,21 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
            {:base, to_charlist(base)},
            {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
            {:scope, :eldap.wholeSubtree()},
-           {:attributes, ['mail', 'email']},
            {:timeout, @search_timeout}
          ]) do
-      {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
-        with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do
-          params = %{
-            email: :erlang.list_to_binary(mail),
-            name: name,
-            nickname: name,
-            password: password,
-            password_confirmation: password
-          }
+      {:ok, {:eldap_search_result, [{:eldap_entry, _, _}], _}} ->
+        params = %{
+          name: name,
+          nickname: name,
+          password: password,
+          password_confirmation: password
+        }
 
-          changeset = User.register_changeset(%User{}, params)
+        changeset = User.register_changeset(%User{}, params)
 
-          case User.register(changeset) do
-            {:ok, user} -> user
-            error -> error
-          end
-        else
-          _ ->
-            Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}")
-            {:error, :ldap_registration_missing_attributes}
+        case User.register(changeset) do
+          {:ok, user} -> user
+          error -> error
         end
 
       error ->
diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs
index 011642c08..76ae461c3 100644
--- a/test/web/oauth/ldap_authorization_test.exs
+++ b/test/web/oauth/ldap_authorization_test.exs
@@ -72,9 +72,7 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
          equalityMatch: fn _type, _value -> :ok end,
          wholeSubtree: fn -> :ok end,
          search: fn _connection, _options ->
-           {:ok,
-            {:eldap_search_result, [{:eldap_entry, '', [{'mail', [to_charlist(user.email)]}]}],
-             []}}
+           {:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}}
          end,
          close: fn _connection ->
            send(self(), :close_connection)

From 0f9aecbca49c828158d2cb549659a68fb21697df Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 08:18:16 -0500
Subject: [PATCH 02/13] Remove fallback to local database when LDAP is
 unavailable.

In many environments this will not work as the LDAP password and the copy stored in Pleroma will stay synchronized.
---
 lib/pleroma/web/auth/ldap_authenticator.ex |  4 --
 test/web/oauth/ldap_authorization_test.exs | 45 ----------------------
 2 files changed, 49 deletions(-)

diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index f320ec746..ec47f6f91 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -28,10 +28,6 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
          %User{} = user <- ldap_user(name, password) do
       {:ok, user}
     else
-      {:error, {:ldap_connection_error, _}} ->
-        # When LDAP is unavailable, try default authenticator
-        @base.get_user(conn)
-
       {:ldap, _} ->
         @base.get_user(conn)
 
diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs
index 76ae461c3..63b1c0eb8 100644
--- a/test/web/oauth/ldap_authorization_test.exs
+++ b/test/web/oauth/ldap_authorization_test.exs
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
   alias Pleroma.Repo
   alias Pleroma.Web.OAuth.Token
   import Pleroma.Factory
-  import ExUnit.CaptureLog
   import Mock
 
   @skip if !Code.ensure_loaded?(:eldap), do: :skip
@@ -99,50 +98,6 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
     end
   end
 
-  @tag @skip
-  test "falls back to the default authorization when LDAP is unavailable" do
-    password = "testpassword"
-    user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password))
-    app = insert(:oauth_app, scopes: ["read", "write"])
-
-    host = Pleroma.Config.get([:ldap, :host]) |> to_charlist
-    port = Pleroma.Config.get([:ldap, :port])
-
-    with_mocks [
-      {:eldap, [],
-       [
-         open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:error, 'connect failed'} end,
-         simple_bind: fn _connection, _dn, ^password -> :ok end,
-         close: fn _connection ->
-           send(self(), :close_connection)
-           :ok
-         end
-       ]}
-    ] do
-      log =
-        capture_log(fn ->
-          conn =
-            build_conn()
-            |> post("/oauth/token", %{
-              "grant_type" => "password",
-              "username" => user.nickname,
-              "password" => password,
-              "client_id" => app.client_id,
-              "client_secret" => app.client_secret
-            })
-
-          assert %{"access_token" => token} = json_response(conn, 200)
-
-          token = Repo.get_by(Token, token: token)
-
-          assert token.user_id == user.id
-        end)
-
-      assert log =~ "Could not open LDAP connection: 'connect failed'"
-      refute_received :close_connection
-    end
-  end
-
   @tag @skip
   test "disallow authorization for wrong LDAP credentials" do
     password = "testpassword"

From d5e4d8a6f3f7b577183809a4b371609aa29fa968 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 09:41:17 -0500
Subject: [PATCH 03/13] Define default authenticator in the config

---
 config/config.exs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/config/config.exs b/config/config.exs
index 933a899ab..257b2e061 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -737,6 +737,8 @@ config :ex_aws, http_client: Pleroma.HTTP.ExAws
 
 config :pleroma, :instances_favicons, enabled: false
 
+config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env()}.exs"

From 2192d1e4920e2c6deffe9a205cc2ade27d4dc0b1 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 10:07:31 -0500
Subject: [PATCH 04/13] Permit LDAP users to register without capturing their
 password hash We don't need it, and local auth fallback has been removed.

---
 lib/pleroma/user.ex                        | 19 +++++++++++++++++++
 lib/pleroma/web/auth/ldap_authenticator.ex |  7 +++----
 2 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 09e606b37..df9f34baa 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -638,6 +638,25 @@ defmodule Pleroma.User do
   @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
   def force_password_reset(user), do: update_password_reset_pending(user, true)
 
+  # Used to auto-register LDAP accounts which don't have a password hash
+  def register_changeset(struct, params = %{password: password})
+      when is_nil(password) do
+    params = Map.put_new(params, :accepts_chat_messages, true)
+
+    struct
+    |> cast(params, [
+      :name,
+      :nickname,
+      :accepts_chat_messages
+    ])
+    |> unique_constraint(:nickname)
+    |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+    |> validate_format(:nickname, local_nickname_regex())
+    |> put_ap_id()
+    |> unique_constraint(:ap_id)
+    |> put_following_and_follower_address()
+  end
+
   def register_changeset(struct, params \\ %{}, opts \\ []) do
     bio_limit = Config.get([:instance, :user_bio_length], 5000)
     name_limit = Config.get([:instance, :user_name_length], 100)
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index ec47f6f91..f667da68b 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -88,7 +88,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
             user
 
           _ ->
-            register_user(connection, base, uid, name, password)
+            register_user(connection, base, uid, name)
         end
 
       error ->
@@ -96,7 +96,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
     end
   end
 
-  defp register_user(connection, base, uid, name, password) do
+  defp register_user(connection, base, uid, name) do
     case :eldap.search(connection, [
            {:base, to_charlist(base)},
            {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
@@ -107,8 +107,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
         params = %{
           name: name,
           nickname: name,
-          password: password,
-          password_confirmation: password
+          password: nil
         }
 
         changeset = User.register_changeset(%User{}, params)

From 81126b0142ec54c785952d0c84a2bdef76965fc7 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 11:36:12 -0500
Subject: [PATCH 05/13] Add email to user account only if it exists in LDAP

---
 lib/pleroma/user.ex                        | 9 +++++++++
 lib/pleroma/web/auth/ldap_authenticator.ex | 8 +++++++-
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index df9f34baa..6d39c9d1b 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -643,12 +643,21 @@ defmodule Pleroma.User do
       when is_nil(password) do
     params = Map.put_new(params, :accepts_chat_messages, true)
 
+    params =
+      if Map.has_key?(params, :email) do
+        Map.put_new(params, :email, params[:email])
+      else
+        params
+      end
+
     struct
     |> cast(params, [
       :name,
       :nickname,
+      :email,
       :accepts_chat_messages
     ])
+    |> validate_required([:name, :nickname])
     |> unique_constraint(:nickname)
     |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
     |> validate_format(:nickname, local_nickname_regex())
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index f667da68b..b1645a359 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -103,13 +103,19 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
            {:scope, :eldap.wholeSubtree()},
            {:timeout, @search_timeout}
          ]) do
-      {:ok, {:eldap_search_result, [{:eldap_entry, _, _}], _}} ->
+      {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
         params = %{
           name: name,
           nickname: name,
           password: nil
         }
 
+        params =
+          case List.keyfind(attributes, 'mail', 0) do
+            {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
+            _ -> params
+          end
+
         changeset = User.register_changeset(%User{}, params)
 
         case User.register(changeset) do

From 2a4bca5bd7e33388193d252f9f956d10ce38ad77 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 11:40:09 -0500
Subject: [PATCH 06/13] Comments are good when they're precise...

---
 lib/pleroma/user.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 6d39c9d1b..69b0e1781 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -638,7 +638,7 @@ defmodule Pleroma.User do
   @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
   def force_password_reset(user), do: update_password_reset_pending(user, true)
 
-  # Used to auto-register LDAP accounts which don't have a password hash
+  # Used to auto-register LDAP accounts which won't have a password hash stored locally
   def register_changeset(struct, params = %{password: password})
       when is_nil(password) do
     params = Map.put_new(params, :accepts_chat_messages, true)

From cb7879c7c148cfc318a176d19b1402e370c509e7 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Wed, 5 Aug 2020 11:53:57 -0500
Subject: [PATCH 07/13] Add note about removal of
 Pleroma.Web.Auth.LDAPAuthenticator fallback to
 Pleroma.Web.Auth.PleromaAuthenticator

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index de017e30a..c0d0fe269 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
 - Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
 - **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated.
+- **Breaking:** LDAP: Fallback to local database authentication has been removed for security reasons and lack of a mechanism to ensure the passwords are synchronized when LDAP passwords are updated.
 
 <details>
   <summary>API Changes</summary>

From e639eee82e1e0136bf6e64e571f2b05b5b7b948c Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Thu, 6 Aug 2020 18:01:29 -0500
Subject: [PATCH 08/13] restricted_nicknames: Add names from MastoAPI endpoints

---
 config/config.exs | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/config/config.exs b/config/config.exs
index 933a899ab..393f74372 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -515,7 +515,13 @@ config :pleroma, Pleroma.User,
     "user-search",
     "user_exists",
     "users",
-    "web"
+    "web",
+    "verify_credentials",
+    "update_credentials",
+    "relationships",
+    "search",
+    "confirmation_resend",
+    "mfa"
   ],
   email_blacklist: []
 

From ebb30128af653d146091fa2317418103fd1e46a3 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 7 Aug 2020 16:20:13 +0200
Subject: [PATCH 09/13] Changelog: Add information about the object age policy

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 572f9e84b..e2a855bef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ## [unreleased]
 
 ### Changed
+- **Breaking:** Added the ObjectAgePolicy to the default set of MRFs. This will delist and strip the follower collection of any message received that is older than 7 days. This will stop users from seeing very old messages in the timelines. The messages can still be viewed on the user's page and in conversations. They also still trigger notifications.
 - **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
 - **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated.
 - In Conversations, return only direct messages as `last_status`

From 6ddea8ebe8794a9626f3907a5d0e0db9604bf949 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Fri, 7 Aug 2020 09:42:10 -0500
Subject: [PATCH 10/13] Add a note about the proper value for uid

---
 docs/configuration/cheatsheet.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index f23cf4fe4..d9115a958 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -890,6 +890,9 @@ Pleroma account will be created with the same name as the LDAP user name.
 * `base`: LDAP base, e.g. "dc=example,dc=com"
 * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
 
+Note, if your LDAP server is an Active Directory server the correct value is commonly `uid: "cn"`, but if you use an
+OpenLDAP server the value may be `uid: "uid"`.
+
 ### OAuth consumer mode
 
 OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).

From 60fe0a08f0ed4b83847995f4e0a5ff10dcf9d336 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 7 Aug 2020 17:59:55 +0200
Subject: [PATCH 11/13] Docs: Remove wrong / confusing auth docs.

---
 docs/configuration/cheatsheet.md | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index f23cf4fe4..89036ded0 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -858,9 +858,6 @@ Warning: it's discouraged to use this feature because of the associated security
 
 ### :auth
 
-* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator.
-* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication.
-
 Authentication / authorization settings.
 
 * `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`.

From 474147a67a89f8bd92186dbda93d78d8e2045d52 Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Fri, 7 Aug 2020 14:54:14 -0500
Subject: [PATCH 12/13] Make a new function instead of overloading
 register_changeset/3

---
 lib/pleroma/user.ex                        | 2 +-
 lib/pleroma/web/auth/ldap_authenticator.ex | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 69b0e1781..d1436a688 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -639,7 +639,7 @@ defmodule Pleroma.User do
   def force_password_reset(user), do: update_password_reset_pending(user, true)
 
   # Used to auto-register LDAP accounts which won't have a password hash stored locally
-  def register_changeset(struct, params = %{password: password})
+  def register_changeset_ldap(struct, params = %{password: password})
       when is_nil(password) do
     params = Map.put_new(params, :accepts_chat_messages, true)
 
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index b1645a359..402ab428b 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -116,7 +116,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
             _ -> params
           end
 
-        changeset = User.register_changeset(%User{}, params)
+        changeset = User.register_changeset_ldap(%User{}, params)
 
         case User.register(changeset) do
           {:ok, user} -> user

From e5557bf8ba6a56996ba8847a522042a748dc046b Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Sat, 8 Aug 2020 16:29:40 +0400
Subject: [PATCH 13/13] Add mix task to add expiration to all local statuses

---
 docs/administration/CLI_tasks/database.md | 12 ++++++-
 lib/mix/tasks/pleroma/database.ex         | 24 +++++++++++---
 lib/pleroma/activity_expiration.ex        | 12 ++++---
 test/tasks/database_test.exs              | 39 +++++++++++++++++++++++
 4 files changed, 77 insertions(+), 10 deletions(-)

diff --git a/docs/administration/CLI_tasks/database.md b/docs/administration/CLI_tasks/database.md
index 647f6f274..64dd66c0c 100644
--- a/docs/administration/CLI_tasks/database.md
+++ b/docs/administration/CLI_tasks/database.md
@@ -97,4 +97,14 @@ but should only be run if necessary. **It is safe to cancel this.**
 
 ```sh tab="From Source"
 mix pleroma.database vacuum full
-```
\ No newline at end of file
+```
+
+## Add expiration to all local statuses
+
+```sh tab="OTP"
+./bin/pleroma_ctl database ensure_expiration
+```
+
+```sh tab="From Source"
+mix pleroma.database ensure_expiration
+```
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index 82e2abdcb..d57e59b11 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -10,6 +10,7 @@ defmodule Mix.Tasks.Pleroma.Database do
   alias Pleroma.User
   require Logger
   require Pleroma.Constants
+  import Ecto.Query
   import Mix.Pleroma
   use Mix.Task
 
@@ -53,8 +54,6 @@ defmodule Mix.Tasks.Pleroma.Database do
   end
 
   def run(["prune_objects" | args]) do
-    import Ecto.Query
-
     {options, [], []} =
       OptionParser.parse(
         args,
@@ -94,8 +93,6 @@ defmodule Mix.Tasks.Pleroma.Database do
   end
 
   def run(["fix_likes_collections"]) do
-    import Ecto.Query
-
     start_pleroma()
 
     from(object in Object,
@@ -130,4 +127,23 @@ defmodule Mix.Tasks.Pleroma.Database do
 
     Maintenance.vacuum(args)
   end
+
+  def run(["ensure_expiration"]) do
+    start_pleroma()
+    days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+
+    Pleroma.Activity
+    |> join(:left, [a], u in assoc(a, :expiration))
+    |> where(local: true)
+    |> where([a, u], is_nil(u))
+    |> Pleroma.RepoStreamer.chunk_stream(100)
+    |> Stream.each(fn activities ->
+      Enum.each(activities, fn activity ->
+        expires_at = Timex.shift(activity.inserted_at, days: days)
+
+        Pleroma.ActivityExpiration.create(activity, expires_at, false)
+      end)
+    end)
+    |> Stream.run()
+  end
 end
diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex
index db9c88d84..7cc9668b3 100644
--- a/lib/pleroma/activity_expiration.ex
+++ b/lib/pleroma/activity_expiration.ex
@@ -20,11 +20,11 @@ defmodule Pleroma.ActivityExpiration do
     field(:scheduled_at, :naive_datetime)
   end
 
-  def changeset(%ActivityExpiration{} = expiration, attrs) do
+  def changeset(%ActivityExpiration{} = expiration, attrs, validate_scheduled_at) do
     expiration
     |> cast(attrs, [:scheduled_at])
     |> validate_required([:scheduled_at])
-    |> validate_scheduled_at()
+    |> validate_scheduled_at(validate_scheduled_at)
   end
 
   def get_by_activity_id(activity_id) do
@@ -33,9 +33,9 @@ defmodule Pleroma.ActivityExpiration do
     |> Repo.one()
   end
 
-  def create(%Activity{} = activity, scheduled_at) do
+  def create(%Activity{} = activity, scheduled_at, validate_scheduled_at \\ true) do
     %ActivityExpiration{activity_id: activity.id}
-    |> changeset(%{scheduled_at: scheduled_at})
+    |> changeset(%{scheduled_at: scheduled_at}, validate_scheduled_at)
     |> Repo.insert()
   end
 
@@ -49,7 +49,9 @@ defmodule Pleroma.ActivityExpiration do
     |> Repo.all()
   end
 
-  def validate_scheduled_at(changeset) do
+  def validate_scheduled_at(changeset, false), do: changeset
+
+  def validate_scheduled_at(changeset, true) do
     validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
       if not expires_late_enough?(scheduled_at) do
         [scheduled_at: "an ephemeral activity must live for at least one hour"]
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
index 883828d77..3a28aa133 100644
--- a/test/tasks/database_test.exs
+++ b/test/tasks/database_test.exs
@@ -127,4 +127,43 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
       assert Enum.empty?(Object.get_by_id(object2.id).data["likes"])
     end
   end
+
+  describe "ensure_expiration" do
+    test "it adds to expiration old statuses" do
+      %{id: activity_id1} = insert(:note_activity)
+
+      %{id: activity_id2} =
+        insert(:note_activity, %{inserted_at: NaiveDateTime.from_iso8601!("2015-01-23 23:50:07")})
+
+      %{id: activity_id3} = activity3 = insert(:note_activity)
+
+      expires_at =
+        NaiveDateTime.utc_now()
+        |> NaiveDateTime.add(60 * 61, :second)
+        |> NaiveDateTime.truncate(:second)
+
+      Pleroma.ActivityExpiration.create(activity3, expires_at)
+
+      Mix.Tasks.Pleroma.Database.run(["ensure_expiration"])
+
+      expirations =
+        Pleroma.ActivityExpiration
+        |> order_by(:activity_id)
+        |> Repo.all()
+
+      assert [
+               %Pleroma.ActivityExpiration{
+                 activity_id: ^activity_id1
+               },
+               %Pleroma.ActivityExpiration{
+                 activity_id: ^activity_id2,
+                 scheduled_at: ~N[2016-01-23 23:50:07]
+               },
+               %Pleroma.ActivityExpiration{
+                 activity_id: ^activity_id3,
+                 scheduled_at: ^expires_at
+               }
+             ] = expirations
+    end
+  end
 end