Fix LDAP auth issues Closes #1646 See merge request pleroma/pleroma!2852note-update
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||||
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated. | - Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated. | ||||
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated. | - Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated. | ||||
- **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated. | - **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated. | ||||
- **Breaking:** LDAP: Fallback to local database authentication has been removed for security reasons and lack of a mechanism to ensure the passwords are synchronized when LDAP passwords are updated. | |||||
<details> | <details> | ||||
<summary>API Changes</summary> | <summary>API Changes</summary> | ||||
@@ -743,6 +743,8 @@ config :ex_aws, http_client: Pleroma.HTTP.ExAws | |||||
config :pleroma, :instances_favicons, enabled: false | config :pleroma, :instances_favicons, enabled: false | ||||
config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator | |||||
# Import environment specific config. This must remain at the bottom | # Import environment specific config. This must remain at the bottom | ||||
# of this file so it overrides the configuration defined above. | # of this file so it overrides the configuration defined above. | ||||
import_config "#{Mix.env()}.exs" | import_config "#{Mix.env()}.exs" |
@@ -887,6 +887,9 @@ Pleroma account will be created with the same name as the LDAP user name. | |||||
* `base`: LDAP base, e.g. "dc=example,dc=com" | * `base`: LDAP base, e.g. "dc=example,dc=com" | ||||
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" | * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" | ||||
Note, if your LDAP server is an Active Directory server the correct value is commonly `uid: "cn"`, but if you use an | |||||
OpenLDAP server the value may be `uid: "uid"`. | |||||
### OAuth consumer mode | ### OAuth consumer mode | ||||
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). | OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). | ||||
@@ -638,6 +638,34 @@ defmodule Pleroma.User do | |||||
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} | @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} | ||||
def force_password_reset(user), do: update_password_reset_pending(user, true) | def force_password_reset(user), do: update_password_reset_pending(user, true) | ||||
# Used to auto-register LDAP accounts which won't have a password hash stored locally | |||||
def register_changeset_ldap(struct, params = %{password: password}) | |||||
when is_nil(password) do | |||||
params = Map.put_new(params, :accepts_chat_messages, true) | |||||
params = | |||||
if Map.has_key?(params, :email) do | |||||
Map.put_new(params, :email, params[:email]) | |||||
else | |||||
params | |||||
end | |||||
struct | |||||
|> cast(params, [ | |||||
:name, | |||||
:nickname, | |||||
:email, | |||||
:accepts_chat_messages | |||||
]) | |||||
|> validate_required([:name, :nickname]) | |||||
|> unique_constraint(:nickname) | |||||
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames])) | |||||
|> validate_format(:nickname, local_nickname_regex()) | |||||
|> put_ap_id() | |||||
|> unique_constraint(:ap_id) | |||||
|> put_following_and_follower_address() | |||||
end | |||||
def register_changeset(struct, params \\ %{}, opts \\ []) do | def register_changeset(struct, params \\ %{}, opts \\ []) do | ||||
bio_limit = Config.get([:instance, :user_bio_length], 5000) | bio_limit = Config.get([:instance, :user_bio_length], 5000) | ||||
name_limit = Config.get([:instance, :user_name_length], 100) | name_limit = Config.get([:instance, :user_name_length], 100) | ||||
@@ -28,10 +28,6 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do | |||||
%User{} = user <- ldap_user(name, password) do | %User{} = user <- ldap_user(name, password) do | ||||
{:ok, user} | {:ok, user} | ||||
else | else | ||||
{:error, {:ldap_connection_error, _}} -> | |||||
# When LDAP is unavailable, try default authenticator | |||||
@base.get_user(conn) | |||||
{:ldap, _} -> | {:ldap, _} -> | ||||
@base.get_user(conn) | @base.get_user(conn) | ||||
@@ -92,7 +88,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do | |||||
user | user | ||||
_ -> | _ -> | ||||
register_user(connection, base, uid, name, password) | |||||
register_user(connection, base, uid, name) | |||||
end | end | ||||
error -> | error -> | ||||
@@ -100,34 +96,31 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do | |||||
end | end | ||||
end | end | ||||
defp register_user(connection, base, uid, name, password) do | |||||
defp register_user(connection, base, uid, name) do | |||||
case :eldap.search(connection, [ | case :eldap.search(connection, [ | ||||
{:base, to_charlist(base)}, | {:base, to_charlist(base)}, | ||||
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, | {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, | ||||
{:scope, :eldap.wholeSubtree()}, | {:scope, :eldap.wholeSubtree()}, | ||||
{:attributes, ['mail', 'email']}, | |||||
{:timeout, @search_timeout} | {:timeout, @search_timeout} | ||||
]) do | ]) do | ||||
{:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} -> | {: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 | |||||
params = %{ | |||||
name: name, | |||||
nickname: name, | |||||
password: nil | |||||
} | |||||
params = | |||||
case List.keyfind(attributes, 'mail', 0) do | |||||
{_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) | |||||
_ -> params | |||||
end | end | ||||
else | |||||
_ -> | |||||
Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}") | |||||
{:error, :ldap_registration_missing_attributes} | |||||
changeset = User.register_changeset_ldap(%User{}, params) | |||||
case User.register(changeset) do | |||||
{:ok, user} -> user | |||||
error -> error | |||||
end | end | ||||
error -> | error -> | ||||
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do | |||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
alias Pleroma.Web.OAuth.Token | alias Pleroma.Web.OAuth.Token | ||||
import Pleroma.Factory | import Pleroma.Factory | ||||
import ExUnit.CaptureLog | |||||
import Mock | import Mock | ||||
@skip if !Code.ensure_loaded?(:eldap), do: :skip | @skip if !Code.ensure_loaded?(:eldap), do: :skip | ||||
@@ -72,9 +71,7 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do | |||||
equalityMatch: fn _type, _value -> :ok end, | equalityMatch: fn _type, _value -> :ok end, | ||||
wholeSubtree: fn -> :ok end, | wholeSubtree: fn -> :ok end, | ||||
search: fn _connection, _options -> | search: fn _connection, _options -> | ||||
{:ok, | |||||
{:eldap_search_result, [{:eldap_entry, '', [{'mail', [to_charlist(user.email)]}]}], | |||||
[]}} | |||||
{:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}} | |||||
end, | end, | ||||
close: fn _connection -> | close: fn _connection -> | ||||
send(self(), :close_connection) | send(self(), :close_connection) | ||||
@@ -102,50 +99,6 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do | |||||
end | end | ||||
@tag @skip | @tag @skip | ||||
test "falls back to the default authorization when LDAP is unavailable" do | |||||
password = "testpassword" | |||||
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) | |||||
app = insert(:oauth_app, scopes: ["read", "write"]) | |||||
host = Pleroma.Config.get([:ldap, :host]) |> to_charlist | |||||
port = Pleroma.Config.get([:ldap, :port]) | |||||
with_mocks [ | |||||
{:eldap, [], | |||||
[ | |||||
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:error, 'connect failed'} end, | |||||
simple_bind: fn _connection, _dn, ^password -> :ok end, | |||||
close: fn _connection -> | |||||
send(self(), :close_connection) | |||||
:ok | |||||
end | |||||
]} | |||||
] do | |||||
log = | |||||
capture_log(fn -> | |||||
conn = | |||||
build_conn() | |||||
|> post("/oauth/token", %{ | |||||
"grant_type" => "password", | |||||
"username" => user.nickname, | |||||
"password" => password, | |||||
"client_id" => app.client_id, | |||||
"client_secret" => app.client_secret | |||||
}) | |||||
assert %{"access_token" => token} = json_response(conn, 200) | |||||
token = Repo.get_by(Token, token: token) | |||||
assert token.user_id == user.id | |||||
end) | |||||
assert log =~ "Could not open LDAP connection: 'connect failed'" | |||||
refute_received :close_connection | |||||
end | |||||
end | |||||
@tag @skip | |||||
test "disallow authorization for wrong LDAP credentials" do | test "disallow authorization for wrong LDAP credentials" do | ||||
password = "testpassword" | password = "testpassword" | ||||
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) | user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) | ||||