From 26b63540953f6a65bb52531b434fd6ab85aaedfe Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivant.business@gmail.com>
Date: Mon, 18 Mar 2019 17:23:38 +0300
Subject: [PATCH] [#923] Support for multiple (external) registrations per user
 via Registration.

---
 config/config.exs                             |  2 +-
 lib/pleroma/registration.ex                   | 36 +++++++++++++
 lib/pleroma/user.ex                           | 16 ++----
 lib/pleroma/web/auth/authenticator.ex         |  6 +--
 lib/pleroma/web/auth/ldap_authenticator.ex    |  2 +-
 lib/pleroma/web/auth/pleroma_authenticator.ex | 51 +++++++++++--------
 lib/pleroma/web/oauth/oauth_controller.ex     |  2 +-
 ...rovider_and_auth_provider_uid_to_users.exs | 12 -----
 .../20190315101315_create_registrations.exs   | 16 ++++++
 9 files changed, 93 insertions(+), 50 deletions(-)
 create mode 100644 lib/pleroma/registration.ex
 delete mode 100644 priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs
 create mode 100644 priv/repo/migrations/20190315101315_create_registrations.exs

diff --git a/config/config.exs b/config/config.exs
index 6839b489bb..03baf894dc 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -381,7 +381,7 @@
   base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
   uid: System.get_env("LDAP_UID") || "cn"
 
-config :pleroma, :auth, oauth_consumer_enabled: false
+config :pleroma, :auth, oauth_consumer_enabled: System.get_env("OAUTH_CONSUMER_ENABLED") == "true"
 
 config :ueberauth,
        Ueberauth,
diff --git a/lib/pleroma/registration.ex b/lib/pleroma/registration.ex
new file mode 100644
index 0000000000..1bd91a3165
--- /dev/null
+++ b/lib/pleroma/registration.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Registration do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+
+  alias Pleroma.Registration
+  alias Pleroma.Repo
+  alias Pleroma.User
+
+  schema "registrations" do
+    belongs_to(:user, User, type: Pleroma.FlakeId)
+    field(:provider, :string)
+    field(:uid, :string)
+    field(:info, :map, default: %{})
+
+    timestamps()
+  end
+
+  def changeset(registration, params \\ %{}) do
+    registration
+    |> cast(params, [:user_id, :provider, :uid, :info])
+    |> foreign_key_constraint(:user_id)
+    |> unique_constraint(:uid, name: :registrations_provider_uid_index)
+  end
+
+  def get_by_provider_uid(provider, uid) do
+    Repo.get_by(Registration,
+      provider: to_string(provider),
+      uid: to_string(uid)
+    )
+  end
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 7f8b282e07..bd742b2fd5 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.User do
   alias Pleroma.Formatter
   alias Pleroma.Notification
   alias Pleroma.Object
+  alias Pleroma.Registration
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web
@@ -41,8 +42,6 @@ defmodule Pleroma.User do
     field(:email, :string)
     field(:name, :string)
     field(:nickname, :string)
-    field(:auth_provider, :string)
-    field(:auth_provider_uid, :string)
     field(:password_hash, :string)
     field(:password, :string, virtual: true)
     field(:password_confirmation, :string, virtual: true)
@@ -56,6 +55,7 @@ defmodule Pleroma.User do
     field(:bookmarks, {:array, :string}, default: [])
     field(:last_refreshed_at, :naive_datetime)
     has_many(:notifications, Notification)
+    has_many(:registrations, Registration)
     embeds_one(:info, Pleroma.User.Info)
 
     timestamps()
@@ -210,13 +210,12 @@ def reset_password(user, data) do
   end
 
   # TODO: FIXME (WIP):
-  def oauth_register_changeset(struct, params \\ %{}) do
+  def external_registration_changeset(struct, params \\ %{}) do
     info_change = User.Info.confirmation_changeset(%User.Info{}, :confirmed)
 
     changeset =
       struct
-      |> cast(params, [:email, :nickname, :name, :bio, :auth_provider, :auth_provider_uid])
-      |> validate_required([:auth_provider, :auth_provider_uid])
+      |> cast(params, [:email, :nickname, :name, :bio])
       |> unique_constraint(:email)
       |> unique_constraint(:nickname)
       |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
@@ -544,13 +543,6 @@ def get_by_nickname_or_email(nickname_or_email) do
     get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
   end
 
-  def get_by_auth_provider_uid(auth_provider, auth_provider_uid),
-    do:
-      Repo.get_by(User,
-        auth_provider: to_string(auth_provider),
-        auth_provider_uid: to_string(auth_provider_uid)
-      )
-
   def get_cached_user_info(user) do
     key = "user_info:#{user.id}"
     Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
index fa439d5626..11f45eec3a 100644
--- a/lib/pleroma/web/auth/authenticator.ex
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -15,10 +15,10 @@ def implementation do
   @callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()}
   def get_user(plug, params), do: implementation().get_user(plug, params)
 
-  @callback get_or_create_user_by_oauth(Plug.Conn.t(), Map.t()) ::
+  @callback get_by_external_registration(Plug.Conn.t(), Map.t()) ::
               {:ok, User.t()} | {:error, any()}
-  def get_or_create_user_by_oauth(plug, params),
-    do: implementation().get_or_create_user_by_oauth(plug, params)
+  def get_by_external_registration(plug, params),
+    do: implementation().get_by_external_registration(plug, params)
 
   @callback handle_error(Plug.Conn.t(), any()) :: any()
   def handle_error(plug, error), do: implementation().handle_error(plug, error)
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index 6c65cff27b..51a0f0fa2f 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -40,7 +40,7 @@ def get_user(%Plug.Conn{} = conn, params) do
     end
   end
 
-  def get_or_create_user_by_oauth(conn, params), do: get_user(conn, params)
+  def get_by_external_registration(conn, params), do: get_user(conn, params)
 
   def handle_error(%Plug.Conn{} = _conn, error) do
     error
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index 2e2bcfb702..2d4399490a 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -5,6 +5,8 @@
 defmodule Pleroma.Web.Auth.PleromaAuthenticator do
   alias Comeonin.Pbkdf2
   alias Pleroma.User
+  alias Pleroma.Registration
+  alias Pleroma.Repo
 
   @behaviour Pleroma.Web.Auth.Authenticator
 
@@ -27,20 +29,21 @@ def get_user(%Plug.Conn{} = _conn, params) do
     end
   end
 
-  def get_or_create_user_by_oauth(
+  def get_by_external_registration(
         %Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}},
         _params
       ) do
-    user = User.get_by_auth_provider_uid(provider, uid)
+    registration = Registration.get_by_provider_uid(provider, uid)
 
-    if user do
+    if registration do
+      user = Repo.preload(registration, :user).user
       {:ok, user}
     else
       info = auth.info
       email = info.email
       nickname = info.nickname
 
-      # TODO: FIXME: connect to existing (non-oauth) account (need a UI flow for that) / generate a random nickname?
+      # Note: nullifying email in case this email is already taken
       email =
         if email && User.get_by_email(email) do
           nil
@@ -48,31 +51,39 @@ def get_or_create_user_by_oauth(
           email
         end
 
+      # Note: generating a random numeric suffix to nickname in case this nickname is already taken
       nickname =
         if nickname && User.get_by_nickname(nickname) do
-          nil
+          "#{nickname}_#{:os.system_time()}"
         else
           nickname
         end
 
-      new_user =
-        User.oauth_register_changeset(
-          %User{},
-          %{
-            auth_provider: to_string(provider),
-            auth_provider_uid: to_string(uid),
-            name: info.name,
-            bio: info.description,
-            email: email,
-            nickname: nickname
-          }
-        )
-
-      Pleroma.Repo.insert(new_user)
+      with {:ok, new_user} <-
+             User.external_registration_changeset(
+               %User{},
+               %{
+                 name: info.name,
+                 bio: info.description,
+                 email: email,
+                 nickname: nickname
+               }
+             )
+             |> Repo.insert(),
+           {:ok, _} <-
+             Registration.changeset(%Registration{}, %{
+               user_id: new_user.id,
+               provider: to_string(provider),
+               uid: to_string(uid),
+               info: %{nickname: info.nickname, email: info.email}
+             })
+             |> Repo.insert() do
+        {:ok, new_user}
+      end
     end
   end
 
-  def get_or_create_user_by_oauth(%Plug.Conn{} = _conn, _params),
+  def get_by_external_registration(%Plug.Conn{} = _conn, _params),
     do: {:error, :missing_credentials}
 
   def handle_error(%Plug.Conn{} = _conn, error) do
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 588933d31f..8c864cb1d4 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -47,7 +47,7 @@ def callback(
         conn,
         %{"client_id" => client_id, "redirect_uri" => redirect_uri} = params
       ) do
-    with {:ok, user} <- Authenticator.get_or_create_user_by_oauth(conn, params) do
+    with {:ok, user} <- Authenticator.get_by_external_registration(conn, params) do
       do_create_authorization(
         conn,
         %{
diff --git a/priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs b/priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs
deleted file mode 100644
index 90947f85a8..0000000000
--- a/priv/repo/migrations/20190315101315_add_auth_provider_and_auth_provider_uid_to_users.exs
+++ /dev/null
@@ -1,12 +0,0 @@
-defmodule Pleroma.Repo.Migrations.AddAuthProviderAndAuthProviderUidToUsers do
-  use Ecto.Migration
-
-  def change do
-    alter table(:users) do
-      add :auth_provider, :string
-      add :auth_provider_uid, :string
-    end
-
-    create unique_index(:users, [:auth_provider, :auth_provider_uid])
-  end
-end
diff --git a/priv/repo/migrations/20190315101315_create_registrations.exs b/priv/repo/migrations/20190315101315_create_registrations.exs
new file mode 100644
index 0000000000..dac86b7805
--- /dev/null
+++ b/priv/repo/migrations/20190315101315_create_registrations.exs
@@ -0,0 +1,16 @@
+defmodule Pleroma.Repo.Migrations.CreateRegistrations do
+  use Ecto.Migration
+
+  def change do
+    create table(:registrations) do
+      add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
+      add :provider, :string
+      add :uid, :string
+      add :info, :map, default: %{}
+
+      timestamps()
+    end
+
+    create unique_index(:registrations, [:provider, :uid])
+  end
+end
-- 
GitLab