Remove exldap dep, dumb ldap user creation, patch tests for LDAP backend
This commit is contained in:
parent
7ea4241f23
commit
acfc0c59a2
7 changed files with 217 additions and 107 deletions
|
@ -23,3 +23,12 @@ config :recycledcloud, RecycledCloudWeb.Endpoint,
|
||||||
|
|
||||||
# Print only warnings and errors during test
|
# Print only warnings and errors during test
|
||||||
config :logger, level: :warn
|
config :logger, level: :warn
|
||||||
|
|
||||||
|
# LDAP configuration
|
||||||
|
config :recycledcloud, :ldap,
|
||||||
|
server: "127.0.0.1",
|
||||||
|
port: 3089,
|
||||||
|
ssl: false,
|
||||||
|
base_dn: "dc=example,dc=org",
|
||||||
|
bind_dn: "cn=admin,dc=example,dc=org",
|
||||||
|
bind_pw: "admin"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
defmodule RecycledCloud.LDAP do
|
defmodule RecycledCloud.LDAP do
|
||||||
require Logger
|
require Logger
|
||||||
require Exldap
|
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
# Every request to the LDAP backend go through this GenServer.
|
# Every request to the LDAP backend go through this GenServer.
|
||||||
|
@ -49,12 +48,39 @@ defmodule RecycledCloud.LDAP do
|
||||||
|
|
||||||
def terminate(reason, state) do
|
def terminate(reason, state) do
|
||||||
if (state.status == :ok), do: close(state.conn)
|
if (state.status == :ok), do: close(state.conn)
|
||||||
IO.inspect(reason)
|
Logger.info "Terminating LDAP backend: #{inspect(reason)}."
|
||||||
#Logger.info "Terminating LDAP backend: #{reason}."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
def search(ldap_conn, field, value, search_timeout \\ 1000) do
|
||||||
|
base_dn = Application.get_env(:recycledcloud, :ldap) |> Keyword.get(:base_dn)
|
||||||
|
opts = [
|
||||||
|
{:base, to_charlist(base_dn)},
|
||||||
|
{:scope, :eldap.wholeSubtree()},
|
||||||
|
{:filter, :eldap.equalityMatch(to_charlist(field), value)},
|
||||||
|
{:timeout, search_timeout}
|
||||||
|
]
|
||||||
|
|
||||||
|
case :eldap.search(ldap_conn, opts) do
|
||||||
|
{:ok, {:eldap_search_result, eldap_result, _}} ->
|
||||||
|
entries = Enum.map(
|
||||||
|
eldap_result,
|
||||||
|
fn entry ->
|
||||||
|
{:eldap_entry, dn, attrs} = entry
|
||||||
|
Enum.reduce(attrs, %{:dn => dn},
|
||||||
|
fn pair, acc ->
|
||||||
|
{key, value} = pair
|
||||||
|
%{List.to_atom(key) => value} |> Map.merge(acc)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, entries}
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
def execute(query), do: GenServer.call(__MODULE__, {:pool, query})
|
def execute(query), do: GenServer.call(__MODULE__, {:pool, query})
|
||||||
def execute_single(query), do: GenServer.call(__MODULE__, {:single, query})
|
def execute_single(query), do: GenServer.call(__MODULE__, {:single, query})
|
||||||
|
|
||||||
|
@ -62,10 +88,32 @@ defmodule RecycledCloud.LDAP do
|
||||||
|
|
||||||
defp internal_execute_query(query, ldap_conn), do: query.(ldap_conn)
|
defp internal_execute_query(query, ldap_conn), do: query.(ldap_conn)
|
||||||
|
|
||||||
|
defp bind(ldap_conn) do
|
||||||
|
conf = Application.get_env(:recycledcloud, :ldap, [])
|
||||||
|
dn = conf |> Keyword.get(:bind_dn)
|
||||||
|
pw = conf |> Keyword.get(:bind_pw)
|
||||||
|
|
||||||
|
:eldap.simple_bind(ldap_conn, dn, pw)
|
||||||
|
end
|
||||||
|
|
||||||
defp connect do
|
defp connect do
|
||||||
case Exldap.connect do
|
conf = Application.get_env(:recycledcloud, :ldap, [])
|
||||||
|
|
||||||
|
host = Keyword.get(conf, :server, "localhost") |> String.to_charlist
|
||||||
|
opts = [
|
||||||
|
{:port, Keyword.get(conf, :port, 389)},
|
||||||
|
{:ssl, Keyword.get(conf, :ssl, false)},
|
||||||
|
{:tcpopts, Keyword.get(conf, :tcpopts, [])}
|
||||||
|
]
|
||||||
|
case :eldap.open([host], opts) do
|
||||||
{:ok, ldap_conn} ->
|
{:ok, ldap_conn} ->
|
||||||
Logger.info "Successfuly connected to LDAP server."
|
Logger.info "Successfuly connected to LDAP server."
|
||||||
|
case bind(ldap_conn) do
|
||||||
|
{:error, err} ->
|
||||||
|
Logger.warning("Could not bind to LDAP server: #{err}")
|
||||||
|
_ -> :noop
|
||||||
|
end
|
||||||
|
|
||||||
%{status: :ok, conn: ldap_conn}
|
%{status: :ok, conn: ldap_conn}
|
||||||
{:error, err} ->
|
{:error, err} ->
|
||||||
Logger.warning "Failed to connect to LDAP server: #{err}. Authentication will not be possible."
|
Logger.warning "Failed to connect to LDAP server: #{err}. Authentication will not be possible."
|
||||||
|
@ -74,7 +122,7 @@ defmodule RecycledCloud.LDAP do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp close(ldap_conn) do
|
defp close(ldap_conn) do
|
||||||
ldap_conn |> Exldap.close
|
ldap_conn |> :eldap.close
|
||||||
Logger.debug("An LDAP connection was closed.")
|
Logger.debug("An LDAP connection was closed.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -63,23 +63,44 @@ defmodule RecycledCloud.Accounts do
|
||||||
## User registration
|
## User registration
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Registers a user.
|
Insert an user in local database.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> register_user(%{field: value})
|
iex> insert_user(%{field: value})
|
||||||
{:ok, %User{}}
|
{:ok, %User{}}
|
||||||
|
|
||||||
iex> register_user(%{field: bad_value})
|
iex> insert_user(%{field: bad_value})
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def register_user(attrs) do
|
def insert_user(attrs) do
|
||||||
%User{}
|
%User{}
|
||||||
|> User.registration_changeset(attrs)
|
|> User.insertion_changeset(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Register an user in LDAP backend and insert in local database.
|
||||||
|
"""
|
||||||
|
def register_user(attrs) do
|
||||||
|
changeset = %User{} |> User.registration_changeset(attrs)
|
||||||
|
case Ecto.Changeset.apply_action(changeset, :update) do
|
||||||
|
{:ok, user} ->
|
||||||
|
case User.register(user) do
|
||||||
|
{:ok, result} -> {:ok, result}
|
||||||
|
{:error, :entryAlreadyExists} ->
|
||||||
|
{
|
||||||
|
:error,
|
||||||
|
Ecto.Changeset.add_error(
|
||||||
|
changeset, :username, "has already been taken"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||||
|
|
||||||
|
@ -90,7 +111,7 @@ defmodule RecycledCloud.Accounts do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def change_user_registration(%User{} = user, attrs \\ %{}) do
|
def change_user_registration(%User{} = user, attrs \\ %{}) do
|
||||||
User.registration_changeset(user, attrs, hash_password: false)
|
User.registration_changeset(user, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
@ -140,10 +161,10 @@ defmodule RecycledCloud.Accounts do
|
||||||
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
||||||
%UserToken{sent_to: email} <- Repo.one(query),
|
%UserToken{sent_to: email} <- Repo.one(query),
|
||||||
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)),
|
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)),
|
||||||
{:ok, _} <- User.set_email(user, email) do
|
:ok <- User.set_email(user, email) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
_ -> :error
|
err -> :error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -188,6 +209,25 @@ defmodule RecycledCloud.Accounts do
|
||||||
User.password_changeset(user, attrs)
|
User.password_changeset(user, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp set_user_password(user, changeset, new_password) do
|
||||||
|
case Ecto.Changeset.apply_action(changeset, :update) do
|
||||||
|
{:ok, _} ->
|
||||||
|
case User.set_password(user, new_password) do
|
||||||
|
:ok ->
|
||||||
|
Repo.delete_all(UserToken.user_and_contexts_query(user, :all))
|
||||||
|
{:ok, user}
|
||||||
|
{:error, err} ->
|
||||||
|
changeset_errors = [current_password: {"Unknown error: #{err}", [err: inspect(err)]}]
|
||||||
|
updated_changeset = changeset
|
||||||
|
|> Map.put(:action, :update)
|
||||||
|
|> Map.put(:errors, changeset_errors)
|
||||||
|
{:error, updated_changeset}
|
||||||
|
end
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates the user password.
|
Updates the user password.
|
||||||
|
|
||||||
|
@ -204,21 +244,8 @@ defmodule RecycledCloud.Accounts do
|
||||||
changeset = user
|
changeset = user
|
||||||
|> User.password_changeset(attrs)
|
|> User.password_changeset(attrs)
|
||||||
|> User.validate_current_password(current_password)
|
|> User.validate_current_password(current_password)
|
||||||
case Ecto.Changeset.apply_action(changeset, :update) do
|
|
||||||
{:ok, _} ->
|
set_user_password(user, changeset, attrs[:password])
|
||||||
new_password = attrs["password"]
|
|
||||||
case User.set_password(user, new_password) do
|
|
||||||
{:ok, _} -> {:ok, user}
|
|
||||||
{:error, err} ->
|
|
||||||
changeset_errors = [current_password: {"Unknown error: #{err}", [err: inspect(err)]}]
|
|
||||||
updated_changeset = changeset
|
|
||||||
|> Map.put(:action, :update)
|
|
||||||
|> Map.put(:errors, changeset_errors)
|
|
||||||
{:error, updated_changeset}
|
|
||||||
end
|
|
||||||
err ->
|
|
||||||
err
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
## Session
|
## Session
|
||||||
|
@ -347,14 +374,10 @@ defmodule RecycledCloud.Accounts do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def reset_user_password(user, attrs) do
|
def reset_user_password(user, attrs) do
|
||||||
Ecto.Multi.new()
|
changeset = user
|
||||||
|> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
|
|> User.password_changeset(attrs)
|
||||||
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
|
|
||||||
|> Repo.transaction()
|
set_user_password(user, changeset, attrs[:password])
|
||||||
|> case do
|
|
||||||
{:ok, %{user: user}} -> {:ok, user}
|
|
||||||
{:error, :user, changeset, _} -> {:error, changeset}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
## Keys
|
## Keys
|
||||||
|
|
|
@ -2,11 +2,13 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
require Logger
|
require Logger
|
||||||
require Exldap
|
|
||||||
alias RecycledCloud.{LDAP,Accounts}
|
alias RecycledCloud.{LDAP,Accounts}
|
||||||
alias RecycledCloud.Accounts.User
|
alias RecycledCloud.Accounts.User
|
||||||
alias RecycledCloud.Repo
|
alias RecycledCloud.Repo
|
||||||
|
|
||||||
|
@min_password_lenght 6
|
||||||
|
@max_password_lenght 80
|
||||||
|
|
||||||
@derive {Inspect, except: [:password]}
|
@derive {Inspect, except: [:password]}
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field :username, :string
|
field :username, :string
|
||||||
|
@ -24,15 +26,10 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
# => ['myusername']}`).
|
# => ['myusername']}`).
|
||||||
defp get_ldap_attributes(%User{username: uid}), do: get_ldap_attributes(uid)
|
defp get_ldap_attributes(%User{username: uid}), do: get_ldap_attributes(uid)
|
||||||
defp get_ldap_attributes(uid) do
|
defp get_ldap_attributes(uid) do
|
||||||
query = fn ldap_conn -> Exldap.search_field(ldap_conn, :uid, uid) end
|
query = fn ldap_conn -> LDAP.search(ldap_conn, :uid, uid) end
|
||||||
case query |> LDAP.execute do
|
case query |> LDAP.execute do
|
||||||
{:ok, []} -> {:error, "could not find matching object"}
|
{:ok, []} -> {:error, "could not find matching object"}
|
||||||
{:ok, [entry]} ->
|
{:ok, [entry]} -> {:ok, entry}
|
||||||
attrs = entry
|
|
||||||
|> Map.get(:attributes)
|
|
||||||
|> Enum.into(%{})
|
|
||||||
|> Map.put('dn', entry.object_name)
|
|
||||||
{:ok, attrs}
|
|
||||||
{:ok, [entry|_]} -> {:error, "found more than one object with uid #{uid}"}
|
{:ok, [entry|_]} -> {:error, "found more than one object with uid #{uid}"}
|
||||||
{:error, err} -> {:error, inspect(err)}
|
{:error, err} -> {:error, inspect(err)}
|
||||||
end
|
end
|
||||||
|
@ -44,7 +41,7 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
# data at the moment, which is inefficient.
|
# data at the moment, which is inefficient.
|
||||||
|
|
||||||
case get_ldap_attributes(user) do
|
case get_ldap_attributes(user) do
|
||||||
{:ok, %{'mail' => [raw_email], 'dn' => raw_dn}} ->
|
{:ok, %{:mail => [raw_email], :dn => raw_dn}} ->
|
||||||
email = List.to_string(raw_email)
|
email = List.to_string(raw_email)
|
||||||
dn = List.to_string(raw_dn)
|
dn = List.to_string(raw_dn)
|
||||||
user |> Map.put(:email, email) |> Map.put(:dn, dn)
|
user |> Map.put(:email, email) |> Map.put(:dn, dn)
|
||||||
|
@ -57,17 +54,58 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp create_ldap_user(%User{} = user) do
|
||||||
|
# FIXME: read dn template from conf, gid_number
|
||||||
|
# FIXME: generate uidNumber
|
||||||
|
conf = Application.get_env(:recycledcloud, :ldap)
|
||||||
|
basetree = conf |> Keyword.get(:base_dn)
|
||||||
|
dn = ("uid=" <> user.username <> ",ou=Users," <> basetree) |> String.to_charlist
|
||||||
|
|
||||||
|
uid_number = 1234
|
||||||
|
gid_number = 1000
|
||||||
|
|
||||||
|
ldif = [
|
||||||
|
{'uid', [user.username]},
|
||||||
|
{'cn', [user.username]},
|
||||||
|
{'givenName', [user.username]},
|
||||||
|
{'sn', [user.username]},
|
||||||
|
{'mail', [user.email]},
|
||||||
|
{'objectClass', ["inetOrgPerson", "posixAccount", "shadowAccount"]},
|
||||||
|
{'homeDirectory', ["/home/#{user.username}"]},
|
||||||
|
{'loginShell', ["/bin/bash"]},
|
||||||
|
{'userPassword', ["/bin/bash"]},
|
||||||
|
{'uidNumber', [Integer.to_string(uid_number)]},
|
||||||
|
{'gidNumber', [Integer.to_string(gid_number)]}
|
||||||
|
]
|
||||||
|
|
||||||
|
query = fn ldap_conn ->
|
||||||
|
case :eldap.add(ldap_conn, dn, ldif) do
|
||||||
|
:ok ->
|
||||||
|
:eldap.modify_password(ldap_conn, dn, to_charlist(user.password))
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
o = query |> LDAP.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def register(%User{} = user) do
|
||||||
|
case create_ldap_user(user) do
|
||||||
|
:ok -> {:ok, get_by_username(user.username)}
|
||||||
|
err -> err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_by_username(username) when is_binary(username) do
|
def get_by_username(username) when is_binary(username) do
|
||||||
local_user = Repo.get_by(User, username: username)
|
local_user = Repo.get_by(User, username: username)
|
||||||
if local_user do
|
if local_user do
|
||||||
local_user |> User.maybe_populate_ldap_attributes()
|
local_user |> User.maybe_populate_ldap_attributes()
|
||||||
else
|
else
|
||||||
case get_ldap_attributes(username) do
|
case get_ldap_attributes(username) do
|
||||||
{:ok, %{'uid' => [raw_uid], 'mail' => [raw_email], 'dn' => raw_dn}} ->
|
{:ok, %{:uid => [raw_uid], :mail => [raw_email], :dn => raw_dn}} ->
|
||||||
uid = List.to_string(raw_uid)
|
uid = List.to_string(raw_uid)
|
||||||
email = List.to_string(raw_email)
|
email = List.to_string(raw_email)
|
||||||
dn = List.to_string(raw_dn)
|
dn = List.to_string(raw_dn)
|
||||||
case Accounts.register_user(%{username: uid, email: email, dn: dn}) do
|
case Accounts.insert_user(%{username: uid, email: email, dn: dn}) do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
user
|
user
|
||||||
{:error, err} ->
|
{:error, err} ->
|
||||||
|
@ -94,6 +132,12 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
also be very expensive to hash for certain algorithms.
|
also be very expensive to hash for certain algorithms.
|
||||||
"""
|
"""
|
||||||
def registration_changeset(user, attrs) do
|
def registration_changeset(user, attrs) do
|
||||||
|
user
|
||||||
|
|> insertion_changeset(attrs)
|
||||||
|
|> validate_password()
|
||||||
|
end
|
||||||
|
|
||||||
|
def insertion_changeset(user, attrs) do
|
||||||
user
|
user
|
||||||
|> cast(attrs, [:username, :password, :email, :dn])
|
|> cast(attrs, [:username, :password, :email, :dn])
|
||||||
|> validate_email()
|
|> validate_email()
|
||||||
|
@ -109,7 +153,7 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
defp validate_password(changeset) do
|
defp validate_password(changeset) do
|
||||||
changeset
|
changeset
|
||||||
|> validate_required([:password])
|
|> validate_required([:password])
|
||||||
|> validate_length(:password, min: 6, max: 80)
|
|> validate_length( :password, min: @min_password_lenght, max: @max_password_lenght)
|
||||||
#|> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
|
#|> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
|
||||||
#|> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
|
#|> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
|
||||||
#|> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|
#|> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|
||||||
|
@ -151,10 +195,10 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
@doc """
|
@doc """
|
||||||
Verifies the password.
|
Verifies the password.
|
||||||
"""
|
"""
|
||||||
def valid_password?(user = %User{username: uid}, password)
|
def valid_password?(%User{} = user, password)
|
||||||
when byte_size(password) > 0 do
|
when byte_size(password) > 0 do
|
||||||
query = fn ldap_conn ->
|
query = fn ldap_conn ->
|
||||||
Exldap.verify_credentials(ldap_conn, user.dn, password)
|
:eldap.simple_bind(ldap_conn, user.dn, password)
|
||||||
end
|
end
|
||||||
|
|
||||||
case query |> LDAP.execute_single do
|
case query |> LDAP.execute_single do
|
||||||
|
@ -178,20 +222,17 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_password(user, new_password) do
|
def set_password(%User{} = user, new_password)
|
||||||
# Exldap does not properly implement `change_password`, hence we have to
|
when byte_size(new_password) > 0 do
|
||||||
# fallback on erlang's `:eldap`.
|
|
||||||
# See http://erlang.org/doc/man/eldap.html#modify-4
|
|
||||||
|
|
||||||
query = fn ldap_conn ->
|
query = fn ldap_conn ->
|
||||||
:eldap.modify_password(
|
:eldap.modify_password(
|
||||||
ldap_conn,
|
ldap_conn,
|
||||||
String.to_charlist(user.dn),
|
to_charlist(user.dn),
|
||||||
String.to_charlist(new_password)
|
to_charlist(new_password)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
query |> LDAP.execute_single
|
query |> LDAP.execute
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_email(user, email) do
|
def set_email(user, email) do
|
||||||
|
@ -201,6 +242,6 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
:eldap.modify(ldap_conn, String.to_charlist(user.dn), [ldif])
|
:eldap.modify(ldap_conn, String.to_charlist(user.dn), [ldif])
|
||||||
end
|
end
|
||||||
|
|
||||||
query |> LDAP.execute_single
|
query |> LDAP.execute
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,7 +85,7 @@ defmodule RecycledCloud.Accounts.UserToken do
|
||||||
query =
|
query =
|
||||||
from token in token_and_context_query(hashed_token, context),
|
from token in token_and_context_query(hashed_token, context),
|
||||||
join: user in assoc(token, :user),
|
join: user in assoc(token, :user),
|
||||||
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
|
where: token.inserted_at > ago(^days, "day") and token.user_id == user.id,
|
||||||
select: user
|
select: user
|
||||||
|
|
||||||
{:ok, query}
|
{:ok, query}
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -45,8 +45,7 @@ defmodule RecycledCloud.MixProject do
|
||||||
{:gettext, "~> 0.11"},
|
{:gettext, "~> 0.11"},
|
||||||
{:jason, "~> 1.0"},
|
{:jason, "~> 1.0"},
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:plug_cowboy, "~> 2.0"},
|
||||||
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false},
|
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false}
|
||||||
{:exldap, git: "https://github.com/Fnux/exldap.git"}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,30 +7,30 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
|
|
||||||
describe "get_user_by_username/1" do
|
describe "get_user_by_username/1" do
|
||||||
test "does not return the user if the username does not exist" do
|
test "does not return the user if the username does not exist" do
|
||||||
refute Accounts.get_user_by_email("unknown")
|
refute Accounts.get_user_by_username("unknown")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns the user if the email exists" do
|
test "returns the user if the username exists" do
|
||||||
%{id: id} = user = user_fixture()
|
%{id: id} = user = user_fixture()
|
||||||
assert %User{id: ^id} = Accounts.get_user_by_username(user.username)
|
assert %User{id: ^id} = Accounts.get_user_by_username(user.username)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_user_by_email_and_password/2" do
|
describe "get_user_by_username_and_password/2" do
|
||||||
test "does not return the user if the email does not exist" do
|
test "does not return the user if the username does not exist" do
|
||||||
refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
|
refute Accounts.get_user_by_username_and_password("unknown", "hello world!")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not return the user if the password is not valid" do
|
test "does not return the user if the password is not valid" do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
refute Accounts.get_user_by_email_and_password(user.email, "invalid")
|
refute Accounts.get_user_by_username_and_password(user.username, "invalid")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns the user if the email and password are valid" do
|
test "returns the user if the username and password are valid" do
|
||||||
%{id: id} = user = user_fixture()
|
%{id: id} = user = user_fixture()
|
||||||
|
|
||||||
assert %User{id: ^id} =
|
assert %User{id: ^id} =
|
||||||
Accounts.get_user_by_email_and_password(user.email, valid_user_password())
|
Accounts.get_user_by_username_and_password(user.username, valid_user_password())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -58,11 +58,11 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates email and password when given" do
|
test "validates email and password when given" do
|
||||||
{:error, changeset} = Accounts.register_user(%{email: "not valid", password: "not valid"})
|
{:error, changeset} = Accounts.register_user(%{email: "not valid", password: "not"})
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
email: ["must have the @ sign and no spaces"],
|
email: ["must have the @ sign and no spaces"],
|
||||||
password: ["should be at least 12 character(s)"]
|
password: ["should be at least 6 character(s)"]
|
||||||
} = errors_on(changeset)
|
} = errors_on(changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -73,23 +73,22 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
assert "should be at most 80 character(s)" in errors_on(changeset).password
|
assert "should be at most 80 character(s)" in errors_on(changeset).password
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates email uniqueness" do
|
test "validates username uniqueness" do
|
||||||
%{email: email} = user_fixture()
|
%{username: username} = user_fixture()
|
||||||
{:error, changeset} = Accounts.register_user(%{email: email})
|
{:error, changeset} = Accounts.register_user(%{
|
||||||
assert "has already been taken" in errors_on(changeset).email
|
username: username,
|
||||||
|
email: unique_user_email(),
|
||||||
|
password: valid_user_password()
|
||||||
|
})
|
||||||
|
assert "has already been taken" in errors_on(changeset).username
|
||||||
|
|
||||||
# Now try with the upper cased email too, to check that email case is ignored.
|
# Now try with the upper cased username too, to check that email case is ignored.
|
||||||
{:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
|
{:error, changeset} = Accounts.register_user(%{
|
||||||
assert "has already been taken" in errors_on(changeset).email
|
username: String.upcase(username),
|
||||||
end
|
email: unique_user_email(),
|
||||||
|
password: valid_user_password()
|
||||||
test "registers users with a hashed password" do
|
})
|
||||||
email = unique_user_email()
|
assert "has already been taken" in errors_on(changeset).username
|
||||||
{:ok, user} = Accounts.register_user(%{email: email, password: valid_user_password()})
|
|
||||||
assert user.email == email
|
|
||||||
assert is_binary(user.hashed_password)
|
|
||||||
assert is_nil(user.confirmed_at)
|
|
||||||
assert is_nil(user.password)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -146,15 +145,6 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validates email uniqueness", %{user: user} do
|
|
||||||
%{email: email} = user_fixture()
|
|
||||||
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.apply_user_email(user, valid_user_password(), %{email: email})
|
|
||||||
|
|
||||||
assert "has already been taken" in errors_on(changeset).email
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates current password", %{user: user} do
|
test "validates current password", %{user: user} do
|
||||||
{:error, changeset} =
|
{:error, changeset} =
|
||||||
Accounts.apply_user_email(user, "invalid", %{email: unique_user_email()})
|
Accounts.apply_user_email(user, "invalid", %{email: unique_user_email()})
|
||||||
|
@ -204,7 +194,7 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
|
|
||||||
test "updates the email with a valid token", %{user: user, token: token, email: email} do
|
test "updates the email with a valid token", %{user: user, token: token, email: email} do
|
||||||
assert Accounts.update_user_email(user, token) == :ok
|
assert Accounts.update_user_email(user, token) == :ok
|
||||||
changed_user = Repo.get!(User, user.id)
|
changed_user = User.get!(user.id)
|
||||||
assert changed_user.email != user.email
|
assert changed_user.email != user.email
|
||||||
assert changed_user.email == email
|
assert changed_user.email == email
|
||||||
assert changed_user.confirmed_at
|
assert changed_user.confirmed_at
|
||||||
|
@ -214,20 +204,20 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
|
|
||||||
test "does not update email with invalid token", %{user: user} do
|
test "does not update email with invalid token", %{user: user} do
|
||||||
assert Accounts.update_user_email(user, "oops") == :error
|
assert Accounts.update_user_email(user, "oops") == :error
|
||||||
assert Repo.get!(User, user.id).email == user.email
|
assert User.get!(user.id).email == user.email
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not update email if user email changed", %{user: user, token: token} do
|
test "does not update email if user email changed", %{user: user, token: token} do
|
||||||
assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) == :error
|
assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) == :error
|
||||||
assert Repo.get!(User, user.id).email == user.email
|
assert User.get!(user.id).email == user.email
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not update email if token expired", %{user: user, token: token} do
|
test "does not update email if token expired", %{user: user, token: token} do
|
||||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
||||||
assert Accounts.update_user_email(user, token) == :error
|
assert Accounts.update_user_email(user, token) == :error
|
||||||
assert Repo.get!(User, user.id).email == user.email
|
assert User.get!(user.id).email == user.email
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
assert Repo.get_by(UserToken, user_id: user.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -258,12 +248,12 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
test "validates password", %{user: user} do
|
test "validates password", %{user: user} do
|
||||||
{:error, changeset} =
|
{:error, changeset} =
|
||||||
Accounts.update_user_password(user, valid_user_password(), %{
|
Accounts.update_user_password(user, valid_user_password(), %{
|
||||||
password: "not valid",
|
password: "not",
|
||||||
password_confirmation: "another"
|
password_confirmation: "another"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
password: ["should be at least 12 character(s)"],
|
password: ["should be at least 6 character(s)"],
|
||||||
password_confirmation: ["does not match password"]
|
password_confirmation: ["does not match password"]
|
||||||
} = errors_on(changeset)
|
} = errors_on(changeset)
|
||||||
end
|
end
|
||||||
|
@ -291,7 +281,7 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
assert is_nil(user.password)
|
assert is_nil(user.password)
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
assert Accounts.get_user_by_username_and_password(user.username, "new valid password")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletes all tokens for the given user", %{user: user} do
|
test "deletes all tokens for the given user", %{user: user} do
|
||||||
|
@ -467,12 +457,12 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
test "validates password", %{user: user} do
|
test "validates password", %{user: user} do
|
||||||
{:error, changeset} =
|
{:error, changeset} =
|
||||||
Accounts.reset_user_password(user, %{
|
Accounts.reset_user_password(user, %{
|
||||||
password: "not valid",
|
password: "not",
|
||||||
password_confirmation: "another"
|
password_confirmation: "another"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
password: ["should be at least 12 character(s)"],
|
password: ["should be at least 6 character(s)"],
|
||||||
password_confirmation: ["does not match password"]
|
password_confirmation: ["does not match password"]
|
||||||
} = errors_on(changeset)
|
} = errors_on(changeset)
|
||||||
end
|
end
|
||||||
|
@ -486,7 +476,7 @@ defmodule RecycledCloud.AccountsTest do
|
||||||
test "updates the password", %{user: user} do
|
test "updates the password", %{user: user} do
|
||||||
{:ok, updated_user} = Accounts.reset_user_password(user, %{password: "new valid password"})
|
{:ok, updated_user} = Accounts.reset_user_password(user, %{password: "new valid password"})
|
||||||
assert is_nil(updated_user.password)
|
assert is_nil(updated_user.password)
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
assert Accounts.get_user_by_username_and_password(user.username, "new valid password")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletes all tokens for the given user", %{user: user} do
|
test "deletes all tokens for the given user", %{user: user} do
|
||||||
|
|
Loading…
Reference in a new issue