commit: 639016bdee37e848b0e72eab5726337c66ecf47f
parent 4557cd960e4345dd218eb0cf07751ea1545e4a27
Author: feld <feld@feld.me>
Date: Fri, 11 Oct 2024 20:48:20 +0000
Merge branch 'refactor-change-password' into 'develop'
LDAP: permit password changing
See merge request pleroma/pleroma!4285
Diffstat:
8 files changed, 114 insertions(+), 39 deletions(-)
diff --git a/changelog.d/ldap-password-change.add b/changelog.d/ldap-password-change.add
@@ -0,0 +1 @@
+LDAP now supports users changing their passwords
diff --git a/installation/openldap/pw_self_service.ldif b/installation/openldap/pw_self_service.ldif
@@ -0,0 +1,7 @@
+dn: olcDatabase={1}mdb,cn=config
+changetype: modify
+add: olcAccess
+olcAccess: {1}to attrs=userPassword
+ by self write
+ by anonymous auth
+ by * none
diff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex
@@ -15,6 +15,14 @@ defmodule Pleroma.LDAP do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
+ def bind_user(name, password) do
+ GenServer.call(__MODULE__, {:bind_user, name, password})
+ end
+
+ def change_password(name, password, new_password) do
+ GenServer.call(__MODULE__, {:change_password, name, password, new_password})
+ end
+
@impl true
def init(state) do
case {Config.get(Pleroma.Web.Auth.Authenticator), Config.get([:ldap, :enabled])} do
@@ -47,33 +55,16 @@ defmodule Pleroma.LDAP do
def handle_info(:connect, _state), do: do_handle_connect()
def handle_info({:bind_after_reconnect, name, password, from}, state) do
- result = bind_user(state[:handle], name, password)
+ result = do_bind_user(state[:handle], name, password)
GenServer.reply(from, result)
{:noreply, state}
end
- defp do_handle_connect do
- state =
- case connect() do
- {:ok, handle} ->
- :eldap.controlling_process(handle, self())
- Process.link(handle)
- [handle: handle]
-
- _ ->
- Logger.error("Failed to connect to LDAP. Retrying in 5000ms")
- Process.send_after(self(), :connect, 5_000)
- []
- end
-
- {:noreply, state}
- end
-
@impl true
def handle_call({:bind_user, name, password}, from, state) do
- case bind_user(state[:handle], name, password) do
+ case do_bind_user(state[:handle], name, password) do
:needs_reconnect ->
Process.send(self(), {:bind_after_reconnect, name, password, from}, [])
{:noreply, state, {:continue, :connect}}
@@ -83,6 +74,12 @@ defmodule Pleroma.LDAP do
end
end
+ def handle_call({:change_password, name, password, new_password}, _from, state) do
+ result = change_password(state[:handle], name, password, new_password)
+
+ {:reply, result, state, :hibernate}
+ end
+
@impl true
def terminate(_, state) do
handle = Keyword.get(state, :handle)
@@ -94,8 +91,21 @@ defmodule Pleroma.LDAP do
:ok
end
- def bind_user(name, password) do
- GenServer.call(__MODULE__, {:bind_user, name, password})
+ defp do_handle_connect do
+ state =
+ case connect() do
+ {:ok, handle} ->
+ :eldap.controlling_process(handle, self())
+ Process.link(handle)
+ [handle: handle]
+
+ _ ->
+ Logger.error("Failed to connect to LDAP. Retrying in 5000ms")
+ Process.send_after(self(), :connect, 5_000)
+ []
+ end
+
+ {:noreply, state}
end
defp connect do
@@ -161,18 +171,17 @@ defmodule Pleroma.LDAP do
end
end
- defp bind_user(handle, name, password) do
- uid = Config.get([:ldap, :uid], "cn")
- base = Config.get([:ldap, :base])
+ defp do_bind_user(handle, name, password) do
+ dn = make_dn(name)
- case :eldap.simple_bind(handle, "#{uid}=#{name},#{base}", password) do
+ case :eldap.simple_bind(handle, dn, password) do
:ok ->
case fetch_user(name) do
%User{} = user ->
user
_ ->
- register_user(handle, base, uid, name)
+ register_user(handle, ldap_base(), ldap_uid(), name)
end
# eldap does not inform us of socket closure
@@ -231,6 +240,14 @@ defmodule Pleroma.LDAP do
end
end
+ defp change_password(handle, name, password, new_password) do
+ dn = make_dn(name)
+
+ with :ok <- :eldap.simple_bind(handle, dn, password) do
+ :eldap.modify_password(handle, dn, to_charlist(new_password), to_charlist(password))
+ end
+ end
+
defp decode_certfile(file) do
with {:ok, data} <- File.read(file) do
data
@@ -242,4 +259,13 @@ defmodule Pleroma.LDAP do
[]
end
end
+
+ defp ldap_uid, do: to_charlist(Config.get([:ldap, :uid], "cn"))
+ defp ldap_base, do: to_charlist(Config.get([:ldap, :base]))
+
+ defp make_dn(name) do
+ uid = ldap_uid()
+ base = ldap_base()
+ ~c"#{uid}=#{name},#{base}"
+ end
end
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
@@ -10,4 +10,9 @@ defmodule Pleroma.Web.Auth.Authenticator do
@callback handle_error(Plug.Conn.t(), any()) :: any()
@callback auth_template() :: String.t() | nil
@callback oauth_consumer_template() :: String.t() | nil
+
+ @callback change_password(Pleroma.User.t(), String.t(), String.t(), String.t()) ::
+ {:ok, Pleroma.User.t()} | {:error, term()}
+
+ @optional_callbacks change_password: 4
end
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -30,4 +30,13 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
error
end
end
+
+ def change_password(user, password, new_password, new_password) do
+ case LDAP.change_password(user.nickname, password, new_password) do
+ :ok -> {:ok, user}
+ e -> e
+ end
+ end
+
+ def change_password(_, _, _, _), do: {:error, :password_confirmation}
end
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.AuthenticationPlug
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
@@ -101,4 +102,23 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def auth_template, do: nil
def oauth_consumer_template, do: nil
+
+ @doc "Changes Pleroma.User password in the database"
+ def change_password(user, password, new_password, new_password) do
+ case CommonAPI.Utils.confirm_current_password(user, password) do
+ {:ok, user} ->
+ with {:ok, _user} <-
+ User.reset_password(user, %{
+ password: new_password,
+ password_confirmation: new_password
+ }) do
+ {:ok, user}
+ end
+
+ error ->
+ error
+ end
+ end
+
+ def change_password(_, _, _, _), do: {:error, :password_confirmation}
end
diff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex
@@ -39,4 +39,8 @@ defmodule Pleroma.Web.Auth.WrapperAuthenticator do
implementation().oauth_consumer_template() ||
Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
end
+
+ @impl true
+ def change_password(user, password, new_password, new_password_confirmation),
+ do: implementation().change_password(user, password, new_password, new_password_confirmation)
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Healthcheck
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
@@ -195,19 +196,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
- case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
- {:ok, user} ->
- with {:ok, _user} <-
- User.reset_password(user, %{
- password: body_params.new_password,
- password_confirmation: body_params.new_password_confirmation
- }) do
- json(conn, %{status: "success"})
- else
- {:error, changeset} ->
- {_, {error, _}} = Enum.at(changeset.errors, 0)
- json(conn, %{error: "New password #{error}."})
- end
+ with {:ok, %User{}} <-
+ Authenticator.change_password(
+ user,
+ body_params.password,
+ body_params.new_password,
+ body_params.new_password_confirmation
+ ) do
+ json(conn, %{status: "success"})
+ else
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {_, {error, _}} = Enum.at(changeset.errors, 0)
+ json(conn, %{error: "New password #{error}."})
+
+ {:error, :password_confirmation} ->
+ json(conn, %{error: "New password does not match confirmation."})
{:error, msg} ->
json(conn, %{error: msg})