OpenLDAP support
authorlink0ff <juri@linkov.net>
Fri, 22 Feb 2019 13:03:43 +0000 (15:03 +0200)
committerlink0ff <juri@linkov.net>
Fri, 22 Feb 2019 13:03:43 +0000 (15:03 +0200)
config/config.exs
docs/config.md
lib/pleroma/ldap.ex [new file with mode: 0644]
lib/pleroma/web/oauth/oauth_controller.ex

index 6119aaea111b3a33d322ffa6312ed4a09394d569..28d93e08697e6b34bc6acf221606df4011b057a2 100644 (file)
@@ -344,6 +344,15 @@ config :pleroma, Pleroma.Jobs,
   federator_outgoing: [max_jobs: 50],
   mailer: [max_jobs: 10]
 
+config :pleroma, :ldap,
+  enabled: System.get_env("LDAP_ENABLED") == "true",
+  host: System.get_env("LDAP_HOST") || "localhost",
+  port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
+  ssl: System.get_env("LDAP_SSL") == "true",
+  sslopts: [],
+  base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
+  uid: System.get_env("LDAP_UID") || "cn"
+
 # 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"
index 14723b727ff5a014e32a279f4b7e7946123c6b6a..a497fb4ca2a88ad3b2e204d5a05a2c356f9fe5d9 100644 (file)
@@ -301,3 +301,12 @@ For each pool, the options are:
 * `max_connections` - how much connections a pool can hold
 * `timeout` - retention duration for connections
 
+## :ldap
+
+* `enabled`: enables LDAP authentication
+* `host`: LDAP server hostname
+* `port`: LDAP port, e.g. 389 or 636
+* `ssl`: true to use SSL
+* `sslopts`: additional SSL options
+* `base`: LDAP base, e.g. "dc=example,dc=com"
+* `uid`: attribute type to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
diff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex
new file mode 100644 (file)
index 0000000..282d8e5
--- /dev/null
@@ -0,0 +1,84 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.LDAP do
+  alias Pleroma.User
+
+  require Logger
+
+  @connection_timeout 10_000
+  @search_timeout 10_000
+
+  def get_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)
+    sslopts = Keyword.get(ldap, :sslopts, [])
+
+    options =
+      [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
+        if sslopts != [], do: [{:sslopts, sslopts}], else: []
+
+    case :eldap.open([to_charlist(host)], options) do
+      {:ok, connection} ->
+        try do
+          uid = Keyword.get(ldap, :uid, "cn")
+          base = Keyword.get(ldap, :base)
+
+          case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
+            :ok ->
+              case User.get_by_nickname_or_email(name) do
+                %User{} = user ->
+                  user
+
+                _ ->
+                  register_user(connection, base, uid, name, password)
+              end
+
+            error ->
+              error
+          end
+        after
+          :eldap.close(connection)
+        end
+
+      {:error, error} ->
+        Logger.error("Could not open LDAP connection: #{inspect(error)}")
+        {:error, {:ldap_connection_error, error}}
+    end
+  end
+
+  def register_user(connection, base, uid, name, password) 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
+      {: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
+          }
+
+          changeset = User.register_changeset(%User{}, params)
+
+          case User.register(changeset) do
+            {:ok, user} -> user
+            error -> error
+          end
+        else
+          _ -> {:error, :ldap_registration_missing_attributes}
+        end
+
+      error ->
+        error
+    end
+  end
+end
index 7c1a3adbd363046ff3606922c113d4518039c41d..654beb2c462a83bfe210c97bfc9aa3bb3ebcf917 100644 (file)
@@ -130,8 +130,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
         %{"grant_type" => "password", "username" => name, "password" => password} = params
       ) do
     with %App{} = app <- get_app_from_request(conn, params),
-         %User{} = user <- User.get_by_nickname_or_email(name),
-         true <- Pbkdf2.checkpw(password, user.password_hash),
+         %User{} = user <- get_user(name, password),
          {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
          scopes <- oauth_scopes(params, app.scopes),
          [] <- scopes -- app.scopes,
@@ -215,4 +214,28 @@ defmodule Pleroma.Web.OAuth.OAuthController do
       nil
     end
   end
+
+  defp get_user(name, password) do
+    if Pleroma.Config.get([:ldap, :enabled]) do
+      case Pleroma.LDAP.get_user(name, password) do
+        %User{} = user ->
+          user
+
+        {:error, {:ldap_connection_error, _}} ->
+          # When LDAP is unavailable, try default login
+          with %User{} = user <- User.get_by_nickname_or_email(name),
+               true <- Pbkdf2.checkpw(password, user.password_hash) do
+            user
+          end
+
+        error ->
+          error
+      end
+    else
+      with %User{} = user <- User.get_by_nickname_or_email(name),
+           true <- Pbkdf2.checkpw(password, user.password_hash) do
+        user
+      end
+    end
+  end
 end