summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMark Felder <feld@feld.me>2024-09-16 00:26:57 -0400
committerMark Felder <feld@feld.me>2024-09-17 13:40:08 -0400
commit9264b21907f5c6890694d6d611ade9b13433463a (patch)
treea693e0a892ad430845fbf1d5ac574cad7c4f9029 /lib
parente7176bb998a7e20f2bb3c9f32e1e2dfe8c3cd818 (diff)
downloadpleroma-9264b21907f5c6890694d6d611ade9b13433463a.tar.gz
pleroma-9264b21907f5c6890694d6d611ade9b13433463a.zip
Pleroma.LDAP
This adds a GenServer which will keep an LDAP connection open and auto reconnect on failure with a 5 second wait between retries. Another benefit is this prevents parsing the Root CAs for every login attempt as we only need to do it once per connection.
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/application.ex1
-rw-r--r--lib/pleroma/ldap.ex233
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex147
3 files changed, 236 insertions, 145 deletions
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index cb15dc1e9..3f199c002 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -94,6 +94,7 @@ defmodule Pleroma.Application do
children =
[
Pleroma.PromEx,
+ Pleroma.LDAP,
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
diff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex
new file mode 100644
index 000000000..868932501
--- /dev/null
+++ b/lib/pleroma/ldap.ex
@@ -0,0 +1,233 @@
+defmodule Pleroma.LDAP do
+ use GenServer
+
+ require Logger
+
+ alias Pleroma.Config
+ alias Pleroma.User
+
+ import Pleroma.Web.Auth.Helpers, only: [fetch_user: 1]
+
+ @connection_timeout 10_000
+ @search_timeout 10_000
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ @impl true
+ def init(state) do
+ case {Config.get(Pleroma.Web.Auth.Authenticator), Config.get([:ldap, :enabled])} do
+ {Pleroma.Web.Auth.LDAPAuthenticator, true} ->
+ {:ok, state, {:continue, :connect}}
+
+ {Pleroma.Web.Auth.LDAPAuthenticator, false} ->
+ Logger.error(
+ "LDAP Authenticator enabled but :pleroma, :ldap is not enabled. Auth will not work."
+ )
+
+ {:ok, state}
+
+ {_, true} ->
+ Logger.warning(
+ ":pleroma, :ldap is enabled but Pleroma.Web.Authenticator is not set to the LDAPAuthenticator. LDAP will not be used."
+ )
+
+ {:ok, state}
+ end
+ end
+
+ @impl true
+ def handle_continue(:connect, _state), do: do_handle_connect()
+
+ @impl true
+ def handle_info(:connect, _state), do: do_handle_connect()
+
+ def handle_info({:bind_after_reconnect, name, password, from}, state) do
+ result = bind_user(state[:connection], name, password)
+
+ GenServer.reply(from, result)
+
+ {:noreply, state}
+ end
+
+ defp do_handle_connect() do
+ state =
+ case connect() do
+ {:ok, connection} ->
+ :eldap.controlling_process(connection, self())
+ [connection: connection]
+
+ _ ->
+ 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[:connection], name, password) do
+ :needs_reconnect ->
+ Process.send(self(), {:bind_after_reconnect, name, password, from}, [])
+ {:noreply, state, {:continue, :connect}}
+
+ result ->
+ {:reply, result, state, :hibernate}
+ end
+ end
+
+ @impl true
+ def terminate(_, state) do
+ :eldap.close(state[:connection])
+
+ :ok
+ end
+
+ defp connect() do
+ ldap = Config.get(:ldap, [])
+ host = Keyword.get(ldap, :host, "localhost")
+ port = Keyword.get(ldap, :port, 389)
+ ssl = Keyword.get(ldap, :ssl, false)
+ tls = Keyword.get(ldap, :tls, false)
+ cacertfile = Keyword.get(ldap, :cacertfile) || CAStore.file_path()
+
+ default_secure_opts = [
+ verify: :verify_peer,
+ cacerts: decode_certfile(cacertfile),
+ customize_hostname_check: [
+ fqdn_fun: fn _ -> to_charlist(host) end
+ ]
+ ]
+
+ sslopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :sslopts, []))
+ tlsopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :tlsopts, []))
+
+ default_options = [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}]
+
+ # :sslopts can only be included in :eldap.open/2 when {ssl: true}
+ # or the connection will fail
+ options =
+ if ssl do
+ default_options ++ [{:sslopts, sslopts}]
+ else
+ default_options
+ end
+
+ case :eldap.open([to_charlist(host)], options) do
+ {:ok, connection} ->
+ try do
+ cond do
+ ssl ->
+ :application.ensure_all_started(:ssl)
+ {:ok, connection}
+
+ tls ->
+ case :eldap.start_tls(
+ connection,
+ tlsopts,
+ @connection_timeout
+ ) do
+ :ok ->
+ {:ok, connection}
+
+ error ->
+ Logger.error("Could not start TLS: #{inspect(error)}")
+ :eldap.close(connection)
+ end
+
+ true ->
+ {:ok, :connection}
+ end
+ after
+ :ok
+ end
+
+ {:error, error} ->
+ Logger.error("Could not open LDAP connection: #{inspect(error)}")
+ {:error, {:ldap_connection_error, error}}
+ end
+ end
+
+ defp bind_user(connection, name, password) do
+ uid = Config.get([:ldap, :uid], "cn")
+ base = Config.get([:ldap, :base])
+
+ case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
+ :ok ->
+ case fetch_user(name) do
+ %User{} = user ->
+ user
+
+ _ ->
+ register_user(connection, base, uid, name)
+ end
+
+ # eldap does not inform us of socket closure
+ # until it is used
+ {:error, {:gen_tcp_error, :closed}} ->
+ :eldap.close(connection)
+ :needs_reconnect
+
+ error ->
+ Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}")
+ {:error, {:ldap_bind_error, error}}
+ end
+ end
+
+ 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))},
+ {:scope, :eldap.wholeSubtree()},
+ {:timeout, @search_timeout}
+ ]) do
+ # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field
+ # https://github.com/erlang/otp/pull/5538
+ {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} ->
+ try_register(name, attributes)
+
+ {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} ->
+ try_register(name, attributes)
+
+ error ->
+ Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}")
+ {:error, {:ldap_search_error, error}}
+ end
+ end
+
+ defp try_register(name, attributes) do
+ params = %{
+ name: name,
+ nickname: name,
+ password: nil
+ }
+
+ params =
+ case List.keyfind(attributes, ~c"mail", 0) do
+ {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
+ _ -> params
+ end
+
+ changeset = User.register_changeset_ldap(%User{}, params)
+
+ case User.register(changeset) do
+ {:ok, user} -> user
+ error -> error
+ end
+ end
+
+ defp decode_certfile(file) do
+ with {:ok, data} <- File.read(file) do
+ data
+ |> :public_key.pem_decode()
+ |> Enum.map(fn {_, b, _} -> b end)
+ else
+ _ ->
+ Logger.error("Unable to read certfile: #{file}")
+ []
+ end
+ end
+end
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
index ad5bc9863..c420c8bc3 100644
--- a/lib/pleroma/web/auth/ldap_authenticator.ex
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -5,16 +5,11 @@
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
alias Pleroma.User
- require Logger
-
- import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
+ import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1]
@behaviour Pleroma.Web.Auth.Authenticator
@base Pleroma.Web.Auth.PleromaAuthenticator
- @connection_timeout 10_000
- @search_timeout 10_000
-
defdelegate get_registration(conn), to: @base
defdelegate create_from_registration(conn, registration), to: @base
defdelegate handle_error(conn, error), to: @base
@@ -24,7 +19,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
def get_user(%Plug.Conn{} = conn) do
with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
{:ok, {name, password}} <- fetch_credentials(conn),
- %User{} = user <- ldap_user(name, password) do
+ %User{} = user <- GenServer.call(Pleroma.LDAP, {:bind_user, name, password}) do
{:ok, user}
else
{:ldap, _} ->
@@ -34,142 +29,4 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
error
end
end
-
- defp ldap_user(name, password) do
- ldap = Pleroma.Config.get(:ldap, [])
- host = Keyword.get(ldap, :host, "localhost")
- port = Keyword.get(ldap, :port, 389)
- ssl = Keyword.get(ldap, :ssl, false)
- tls = Keyword.get(ldap, :tls, false)
- cacertfile = Keyword.get(ldap, :cacertfile) || CAStore.file_path()
-
- default_secure_opts = [
- verify: :verify_peer,
- cacerts: decode_certfile(cacertfile),
- customize_hostname_check: [
- fqdn_fun: fn _ -> to_charlist(host) end
- ]
- ]
-
- sslopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :sslopts, []))
- tlsopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :tlsopts, []))
-
- # :sslopts can only be included in :eldap.open/2 when {ssl: true}
- # or the connection will fail
- options =
- if ssl do
- [{:port, port}, {:ssl, ssl}, {:sslopts, sslopts}, {:timeout, @connection_timeout}]
- else
- [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}]
- end
-
- case :eldap.open([to_charlist(host)], options) do
- {:ok, connection} ->
- try do
- cond do
- ssl ->
- :application.ensure_all_started(:ssl)
-
- tls ->
- case :eldap.start_tls(
- connection,
- tlsopts,
- @connection_timeout
- ) do
- :ok ->
- :ok
-
- error ->
- Logger.error("Could not start TLS: #{inspect(error)}")
- :eldap.close(connection)
- end
-
- true ->
- :ok
- end
-
- bind_user(connection, ldap, name, password)
- after
- :eldap.close(connection)
- end
-
- {:error, error} ->
- Logger.error("Could not open LDAP connection: #{inspect(error)}")
- {:error, {:ldap_connection_error, error}}
- end
- end
-
- defp bind_user(connection, ldap, name, password) do
- uid = Keyword.get(ldap, :uid, "cn")
- base = Keyword.get(ldap, :base)
-
- case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
- :ok ->
- case fetch_user(name) do
- %User{} = user ->
- user
-
- _ ->
- register_user(connection, base, uid, name)
- end
-
- error ->
- Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}")
- {:error, {:ldap_bind_error, error}}
- end
- end
-
- 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))},
- {:scope, :eldap.wholeSubtree()},
- {:timeout, @search_timeout}
- ]) do
- # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field
- # https://github.com/erlang/otp/pull/5538
- {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} ->
- try_register(name, attributes)
-
- {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} ->
- try_register(name, attributes)
-
- error ->
- Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}")
- {:error, {:ldap_search_error, error}}
- end
- end
-
- defp try_register(name, attributes) do
- params = %{
- name: name,
- nickname: name,
- password: nil
- }
-
- params =
- case List.keyfind(attributes, ~c"mail", 0) do
- {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
- _ -> params
- end
-
- changeset = User.register_changeset_ldap(%User{}, params)
-
- case User.register(changeset) do
- {:ok, user} -> user
- error -> error
- end
- end
-
- defp decode_certfile(file) do
- with {:ok, data} <- File.read(file) do
- data
- |> :public_key.pem_decode()
- |> Enum.map(fn {_, b, _} -> b end)
- else
- _ ->
- Logger.error("Unable to read certfile: #{file}")
- []
- end
- end
end