diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pleroma/application.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/config/transfer_task.ex | 3 | ||||
| -rw-r--r-- | lib/pleroma/ldap.ex | 241 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/ldap_authenticator.ex | 147 | 
4 files changed, 246 insertions, 146 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/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index ffc95f144..140dd7711 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -22,7 +22,8 @@ defmodule Pleroma.Config.TransferTask do        {:pleroma, :markup},        {:pleroma, :streamer},        {:pleroma, :pools}, -      {:pleroma, :connections_pool} +      {:pleroma, :connections_pool}, +      {:pleroma, :ldap}      ]    defp reboot_time_subkeys, diff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex new file mode 100644 index 000000000..cd84dee02 --- /dev/null +++ b/lib/pleroma/ldap.ex @@ -0,0 +1,241 @@ +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 2_000 +  @search_timeout 2_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} + +      _ -> +        {: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[: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 +      :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 +    handle = Keyword.get(state, :handle) + +    if not is_nil(handle) do +      :eldap.close(handle) +    end + +    :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() + +    if ssl, do: Application.ensure_all_started(:ssl) + +    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, handle} -> +        try do +          cond do +            tls -> +              case :eldap.start_tls( +                     handle, +                     tlsopts, +                     @connection_timeout +                   ) do +                :ok -> +                  {:ok, handle} + +                error -> +                  Logger.error("Could not start TLS: #{inspect(error)}") +                  :eldap.close(handle) +              end + +            true -> +              {:ok, handle} +          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(handle, name, password) do +    uid = Config.get([:ldap, :uid], "cn") +    base = Config.get([:ldap, :base]) + +    case :eldap.simple_bind(handle, "#{uid}=#{name},#{base}", password) do +      :ok -> +        case fetch_user(name) do +          %User{} = user -> +            user + +          _ -> +            register_user(handle, base, uid, name) +        end + +      # eldap does not inform us of socket closure +      # until it is used +      {:error, {:gen_tcp_error, :closed}} -> +        :eldap.close(handle) +        :needs_reconnect + +      {:error, error} = e -> +        Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}") +        e +    end +  end + +  defp register_user(handle, base, uid, name) do +    case :eldap.search(handle, [ +           {: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 +    mail_attribute = Config.get([:ldap, :mail]) + +    params = %{ +      name: name, +      nickname: name, +      password: nil +    } + +    params = +      case List.keyfind(attributes, to_charlist(mail_attribute), 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 | 
