defmodule Paddle.Parsing do
@moduledoc ~S"""
Module composed of utility functions for translating between `:eldap` and
`Paddle` representation.
"""
# =====================
# == DN manipulation ==
# =====================
@spec construct_dn(keyword | [{binary, binary}], binary | charlist) :: charlist
@doc ~S"""
Construct a DN Erlang string based on a keyword list or a string.
Examples:
iex> Paddle.Parsing.construct_dn(uid: "user", ou: "People")
'uid=user,ou=People'
iex> Paddle.Parsing.construct_dn([{"uid", "user"}, {"ou", "People"}], "dc=organisation,dc=org")
'uid=user,ou=People,dc=organisation,dc=org'
iex> Paddle.Parsing.construct_dn("uid=user,ou=People", "dc=organisation,dc=org")
'uid=user,ou=People,dc=organisation,dc=org'
Values are escaped.
Note: using a map is highly discouraged because the key / values may be
reordered and because they can be mistaken for a class object (see
`Paddle.Class`).
"""
def construct_dn(map, base \\ '')
def construct_dn(subdn, base) when is_binary(base) do
construct_dn(subdn, :binary.bin_to_list(base))
end
def construct_dn([], base) when is_list(base), do: base
def construct_dn(subdn, base) when is_binary(subdn) and is_list(base),
do: :binary.bin_to_list(subdn) ++ ',' ++ base
def construct_dn(nil, base) when is_list(base), do: base
def construct_dn(map, '') do
',' ++ dn =
Enum.reduce(
map,
'',
fn {key, value}, acc ->
acc ++ ',#{key}=#{ldap_escape(value)}'
end
)
dn
end
def construct_dn(map, base) when is_list(base) do
construct_dn(map, '') ++ ',' ++ base
end
@spec dn_to_kwlist(charlist | binary) :: [{binary, binary}]
@doc ~S"""
Tranform an LDAP DN to a keyword list.
Well, not exactly a keyword list but a list like this:
[{"uid", "user"}, {"ou", "People"}, {"dc", "organisation"}, {"dc", "org"}]
Example:
iex> Paddle.Parsing.dn_to_kwlist("uid=user,ou=People,dc=organisation,dc=org")
[{"uid", "user"}, {"ou", "People"}, {"dc", "organisation"}, {"dc", "org"}]
"""
def dn_to_kwlist(""), do: []
def dn_to_kwlist(nil), do: []
def dn_to_kwlist(dn) when is_binary(dn) do
%{"key" => key, "value" => value, "rest" => rest} =
Regex.named_captures(~r/^(?<key>.+)=(?<value>.+)(,(?<rest>.+))?$/U, dn)
[{key, value}] ++ dn_to_kwlist(rest)
end
def dn_to_kwlist(dn), do: dn_to_kwlist(List.to_string(dn))
@spec ldap_escape(charlist | binary) :: charlist
@doc ~S"""
Escape special LDAP characters in a string.
Example:
iex> Paddle.Parsing.ldap_escape("a=b#c\\")
'a\\=b\\#c\\\\'
"""
def ldap_escape(''), do: ''
def ldap_escape([char | rest]) do
escaped_char =
case char do
?, -> '\\,'
?# -> '\\#'
?+ -> '\\+'
?< -> '\\<'
?> -> '\\>'
?; -> '\\;'
?" -> '\\\"'
?= -> '\\='
?\\ -> '\\\\'
_ -> [char]
end
escaped_char ++ ldap_escape(rest)
end
def ldap_escape(token), do: ldap_escape(:binary.bin_to_list(token))
# =============
# == Entries ==
# =============
@type eldap_dn :: charlist
@type eldap_entry :: {:eldap_entry, eldap_dn, [{charlist, [charlist]}]}
@spec clean_eldap_search_results(
{:ok, {:eldap_search_result, [eldap_entry]}}
| {:error, atom},
charlist
) :: {:ok, [Paddle.ldap_entry()]} | {:error, Paddle.search_ldap_error()}
@doc ~S"""
Convert an `:eldap` search result to a `Paddle` representation.
Also see `clean_entries/1`
Examples:
iex> eldap_entry = {:eldap_entry, 'uid=testuser,ou=People', [{'uid', ['testuser']}]}
iex> Paddle.Parsing.clean_eldap_search_results({:ok, {:eldap_search_result, [eldap_entry], []}}, '')
{:ok, [%{"dn" => "uid=testuser,ou=People", "uid" => ["testuser"]}]}
iex> Paddle.Parsing.clean_eldap_search_results({:ok, {:eldap_search_result, [], []}}, '')
{:error, :noSuchObject}
iex> Paddle.Parsing.clean_eldap_search_results({:error, :insufficientAccessRights}, '')
{:error, :insufficientAccessRights}
iex> eldap_entry = {:eldap_entry, 'uid=testuser,ou=People', [{'uid', ['testuser']}]}
iex> Paddle.Parsing.clean_eldap_search_results({:ok, {:eldap_search_result, [eldap_entry], []}}, 'ou=People')
{:ok, [%{"dn" => "uid=testuser", "uid" => ["testuser"]}]}
"""
def clean_eldap_search_results({:error, error}, _base) do
{:error, error}
end
def clean_eldap_search_results({:ok, {:eldap_search_result, [], []}}, _base) do
{:error, :noSuchObject}
end
def clean_eldap_search_results({:ok, {:eldap_search_result, [], [], :asn1_NOVALUE}}, _base) do
{:error, :noSuchObject}
end
# I have no idea why this is happening.
def clean_eldap_search_results({:ok, {:eldap_search_result, entries, [], :asn1_NOVALUE}}, base) do
{:ok, clean_entries(entries, base)}
end
def clean_eldap_search_results({:ok, {:eldap_search_result, entries, []}}, base) do
{:ok, clean_entries(entries, base)}
end
@spec entry_to_class_object(Paddle.ldap_entry(), Paddle.Class.t()) :: Paddle.Class.t()
@doc ~S"""
Convert a `Paddle` entry to a given `Paddle` class object.
Example:
iex> entry = %{"dn" => "uid=testuser,ou=People", "uid" => ["testuser"], "description" => ["hello"]}
iex> Paddle.Parsing.entry_to_class_object(entry, %MyApp.PosixAccount{})
%MyApp.PosixAccount{cn: nil, description: ["hello"], gecos: nil,
gidNumber: nil, homeDirectory: nil, host: nil, l: nil,
loginShell: nil, o: nil, ou: nil, seeAlso: nil, uid: ["testuser"],
uidNumber: nil, userPassword: nil}
"""
def entry_to_class_object(entry, target) do
entry =
entry
|> Map.drop(["dn", "objectClass"])
|> Enum.map(fn {key, value} -> {String.to_atom(key), value} end)
|> Enum.into(%{})
Map.merge(target, entry)
end
@spec clean_entries([eldap_entry], charlist) :: [Paddle.ldap_entry()]
@doc ~S"""
Get a binary map representation of several eldap entries.
The `base` argument corresponds to the DN base which should be stripped from
the result's `"dn"` attribute.
Example:
iex> Paddle.Parsing.clean_entries([{:eldap_entry, 'uid=testuser,ou=People', [{'uid', ['testuser']}]}], '')
[%{"dn" => "uid=testuser,ou=People", "uid" => ["testuser"]}]
iex> Paddle.Parsing.clean_entries([{:eldap_entry, 'uid=testuser,ou=People', [{'uid', ['testuser']}]}], 'ou=People')
[%{"dn" => "uid=testuser", "uid" => ["testuser"]}]
"""
def clean_entries(entries, base) do
base_length = length(base)
entries
|> Enum.map(&clean_entry(&1, base_length))
end
@spec clean_entry(eldap_entry, integer) :: Paddle.ldap_entry()
@doc ~S"""
Get a binary map representation of a single eldap entry.
The `base_length` argument corresponds to the DN base length which should be
stripped from the result's `"dn"` attribute.
Example:
iex> Paddle.Parsing.clean_entry({:eldap_entry, 'uid=testuser,ou=People', [{'uid', ['testuser']}]}, 0)
%{"dn" => "uid=testuser,ou=People", "uid" => ["testuser"]}
iex> Paddle.Parsing.clean_entry({:eldap_entry, 'uid=testuser,ou=People', [{'uid', ['testuser']}]}, 9)
%{"dn" => "uid=testuser", "uid" => ["testuser"]}
"""
def clean_entry({:eldap_entry, dn, attributes}, base_length) do
%{"dn" => dn |> List.to_string() |> strip_base_from_dn(base_length)}
|> Map.merge(
attributes
|> attributes_to_binary
|> Enum.into(%{})
)
end
defp strip_base_from_dn(dn, 0) when is_binary(dn), do: dn
defp strip_base_from_dn(dn, base_length) when is_binary(dn) do
dn_length = String.length(dn)
String.slice(dn, 0, dn_length - base_length - 1)
end
# ===================
# == Modifications ==
# ===================
@spec mod_convert(Paddle.mod()) :: tuple
@doc ~S"""
Convert a user-friendly modify operation to an eldap operation.
Examples:
iex> Paddle.Parsing.mod_convert {:add, {"description", "This is a description"}}
{:ModifyRequest_changes_SEQOF, :add,
{:PartialAttribute, 'description', ['This is a description']}}
iex> Paddle.Parsing.mod_convert {:delete, "description"}
{:ModifyRequest_changes_SEQOF, :delete,
{:PartialAttribute, 'description', []}}
iex> Paddle.Parsing.mod_convert {:replace, {"description", "This is a description"}}
{:ModifyRequest_changes_SEQOF, :replace,
{:PartialAttribute, 'description', ['This is a description']}}
"""
def mod_convert(operation)
def mod_convert({:add, {field, value}}) do
field = '#{field}'
value = list_wrap(value)
:eldap.mod_add(field, value)
end
def mod_convert({:delete, field}) do
field = '#{field}'
:eldap.mod_delete(field, [])
end
def mod_convert({:replace, {field, value}}) do
field = '#{field}'
value = list_wrap(value)
:eldap.mod_replace(field, value)
end
# ===================
# == Miscellaneous ==
# ===================
@spec list_wrap(term) :: [charlist]
@doc ~S"""
Wrap things in lists and convert binaries / atoms to charlists.
iex> Paddle.Parsing.list_wrap "hello"
['hello']
iex> Paddle.Parsing.list_wrap :hello
['hello']
iex> Paddle.Parsing.list_wrap ["hello", "world"]
['hello', 'world']
"""
def list_wrap(list) when is_list(list), do: list |> Enum.map(&'#{&1}')
def list_wrap(thing), do: ['#{thing}']
# =======================
# == Private Utilities ==
# =======================
@spec attributes_to_binary([{charlist, [charlist]}]) :: [{binary, [binary]}]
defp attributes_to_binary(attributes) do
attributes
|> Enum.map(&attribute_to_binary/1)
end
@spec attribute_to_binary({charlist, [charlist]}) :: {binary, [binary]}
defp attribute_to_binary({key, values}) do
{List.to_string(key), values |> Enum.map(&:binary.list_to_bin/1)}
end
end