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/^(?.+)=(?.+)(,(?.+))?$/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 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