From 935736fb191f7b889e59d56c19617403d8a63acb Mon Sep 17 00:00:00 2001 From: Guilherme Balena Versiani Date: Tue, 31 May 2022 19:38:41 +0000 Subject: [PATCH] * Mix format; * Fixed head declaration of `Paddle.Parsing.construct_dn/2`; * Removed deprecated `Enum.filter_map/3` usages; * Removed `Warning: variable 'Str' shadowed in 'fun'` from `src/schema_parser.yrl`; * Fixed `Paddle.Parsing.clean_eldap_search_results/2` for the cases where the resulting tuple is larger than expected. --- config/config.exs | 4 +- config/dev.exs | 2 +- config/test.exs | 2 +- lib/paddle.ex | 280 ++++++++++++++++++++---------------- lib/paddle/application.ex | 6 +- lib/paddle/attributes.ex | 13 +- lib/paddle/class.ex | 58 ++++---- lib/paddle/filters.ex | 17 ++- lib/paddle/parsing.ex | 119 +++++++-------- lib/paddle/schema_parser.ex | 50 ++++--- mix.exs | 51 ++++--- src/schema_parser.yrl | 4 +- test/paddle_test.exs | 32 ++++- test/support/classes.ex | 46 +++--- 14 files changed, 385 insertions(+), 299 deletions(-) diff --git a/config/config.exs b/config/config.exs index aeb17df..12644e7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this @@ -34,4 +34,4 @@ config :paddle, Paddle, # Configuration from the imported file will override the ones defined # here (which is why it is important to import them last). # -import_config "#{Mix.env}.exs" +import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index 5460823..a9a3f51 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :paddle, Paddle, host: "localhost", diff --git a/config/test.exs b/config/test.exs index 354972c..b13af1b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :paddle, Paddle, schema_files: Path.wildcard("/etc/ldap/schema/*.schema"), diff --git a/lib/paddle.ex b/lib/paddle.ex index 609c286..f219322 100644 --- a/lib/paddle.ex +++ b/lib/paddle.ex @@ -107,7 +107,7 @@ defmodule Paddle do alias Paddle.Filters alias Paddle.Attributes - @typep ldap_conn :: :eldap.handle | {:not_connected, binary} + @typep ldap_conn :: :eldap.handle() | {:not_connected, binary} @type ldap_entry :: %{required(binary) => binary} @type auth_status :: :ok | {:error, atom} @@ -120,7 +120,7 @@ defmodule Paddle do """ end - @spec start_link(term) :: Genserver.on_start + @spec start_link(term) :: Genserver.on_start() @doc false def start_link(opts \\ []) do @@ -153,14 +153,18 @@ defmodule Paddle do Logger.info("Stopped LDAP") end - @spec handle_call({:authenticate, charlist, charlist} | - {:reconnect, list} | - {:get, Paddle.Filters.t, dn, atom} | - {:get_single, Paddle.Filters.t, dn, atom} | - {:add, dn, attributes, atom} | - {:delete, dn, atom} | - {:modify, dn, atom, [mod]}, GenServer.from, ldap_conn) :: - {:reply, term, ldap_conn} + @spec handle_call( + {:authenticate, charlist, charlist} + | {:reconnect, list} + | {:get, Paddle.Filters.t(), dn, atom} + | {:get_single, Paddle.Filters.t(), dn, atom} + | {:add, dn, attributes, atom} + | {:delete, dn, atom} + | {:modify, dn, atom, [mod]}, + GenServer.from(), + ldap_conn + ) :: + {:reply, term, ldap_conn} @impl GenServer def handle_call({:reconnect, opts}, _from, ldap_conn) do @@ -184,7 +188,7 @@ defmodule Paddle do @impl GenServer def handle_call({:authenticate, dn, password}, _from, ldap_conn) do - Logger.debug "Authenticating with dn: #{dn}" + Logger.debug("Authenticating with dn: #{dn}") status = :eldap.simple_bind(ldap_conn, dn, password) case status do @@ -196,33 +200,34 @@ defmodule Paddle do @impl GenServer def handle_call({:get, filter, kwdn, base}, _from, ldap_conn) do base = config(base) - dn = Parsing.construct_dn(kwdn, base) + dn = Parsing.construct_dn(kwdn, base) filter = Filters.construct_filter(filter) - Logger.debug("Getting entries with dn: #{dn} and filter: #{inspect filter, pretty: true}") + Logger.debug("Getting entries with dn: #{dn} and filter: #{inspect(filter, pretty: true)}") {:reply, :eldap.search(ldap_conn, base: dn, filter: filter) - |> Parsing.clean_eldap_search_results(base), - ldap_conn} + |> Parsing.clean_eldap_search_results(base), ldap_conn} end @impl GenServer def handle_call({:get_single, filter, kwdn, base}, _from, ldap_conn) do base = config(base) - dn = Parsing.construct_dn(kwdn, base) + dn = Parsing.construct_dn(kwdn, base) filter = Filters.construct_filter(filter) - Logger.debug("Getting single entry with dn: #{dn} and filter: #{inspect filter, pretty: true}") + Logger.debug( + "Getting single entry with dn: #{dn} and filter: #{inspect(filter, pretty: true)}" + ) {:reply, :eldap.search(ldap_conn, - base: dn, - scope: :eldap.baseObject, - filter: filter) - |> Parsing.clean_eldap_search_results(base) - |> ensure_single_result, - ldap_conn} + base: dn, + scope: :eldap.baseObject(), + filter: filter + ) + |> Parsing.clean_eldap_search_results(base) + |> ensure_single_result, ldap_conn} end @impl GenServer @@ -231,9 +236,10 @@ defmodule Paddle do Logger.info("Adding entry with dn: #{dn}") - attributes = attributes - |> Enum.filter_map(fn {_key, value} -> value != nil end, - fn {key, value} -> {'#{key}', Parsing.list_wrap value} end) + attributes = + attributes + |> Enum.filter(fn {_key, value} -> value != nil end) + |> Enum.map(fn {key, value} -> {'#{key}', Parsing.list_wrap(value)} end) {:reply, :eldap.add(ldap_conn, dn, attributes), ldap_conn} end @@ -251,17 +257,24 @@ defmodule Paddle do def handle_call({:modify, kwdn, base, mods}, _from, ldap_conn) do dn = Parsing.construct_dn(kwdn, config(base)) - Logger.info("Modifying entry: \"#{dn}\" with mods: #{inspect mods}") + Logger.info("Modifying entry: \"#{dn}\" with mods: #{inspect(mods)}") mods = mods |> Enum.map(&Parsing.mod_convert/1) {:reply, :eldap.modify(ldap_conn, dn, mods), ldap_conn} end - @type authenticate_ldap_error :: :operationsError | :protocolError | - :authMethodNotSupported | :strongAuthRequired | :referral | - :saslBindInProgress | :inappropriateAuthentication | :invalidCredentials | - :unavailable | :anonymous_auth + @type authenticate_ldap_error :: + :operationsError + | :protocolError + | :authMethodNotSupported + | :strongAuthRequired + | :referral + | :saslBindInProgress + | :inappropriateAuthentication + | :invalidCredentials + | :unavailable + | :anonymous_auth @spec authenticate(dn, binary) :: :ok | {:error, authenticate_ldap_error} @doc ~S""" @@ -309,7 +322,7 @@ defmodule Paddle do GenServer.call(Paddle, {:reconnect, opts}) end - @spec get_dn(Paddle.Class.t) :: {:ok, binary} | {:error, :missing_unique_identifier} + @spec get_dn(Paddle.Class.t()) :: {:ok, binary} | {:error, :missing_unique_identifier} @doc ~S""" Get the DN of an entry. @@ -326,7 +339,7 @@ defmodule Paddle do id_value = Map.get(object, id_field) if id_value do - id_value = Paddle.Parsing.ldap_escape id_value + id_value = Paddle.Parsing.ldap_escape(id_value) {:ok, "#{id_field}=#{id_value},#{subdn}"} else @@ -338,8 +351,12 @@ defmodule Paddle do # == Getting == # ============= - @type search_ldap_error :: :noSuchObject | :sizeLimitExceeded | - :timeLimitExceeded | :undefinedAttributeType | :insufficientAccessRights + @type search_ldap_error :: + :noSuchObject + | :sizeLimitExceeded + | :timeLimitExceeded + | :undefinedAttributeType + | :insufficientAccessRights @spec get(dn) :: {:ok, [ldap_entry]} | {:error, search_ldap_error} @@ -385,15 +402,15 @@ defmodule Paddle do "userPassword" => ["{SSHA}AIzygLSXlArhAMzddUriXQxf7UlkqopP"]}]} """ def get(kwdn) when is_list(kwdn) do - GenServer.call(Paddle, - {:get, - Keyword.get(kwdn, :filter), - Keyword.get(kwdn, :base), - :base}) + GenServer.call( + Paddle, + {:get, Keyword.get(kwdn, :filter), Keyword.get(kwdn, :base), :base} + ) end - @spec get(Paddle.Class.t) :: {:ok, [Paddle.Class.t]} | {:error, search_ldap_error} - @spec get(Paddle.Class.t, Paddle.Filters.t) :: {:ok, [Paddle.Class.t]} | {:error, search_ldap_error} + @spec get(Paddle.Class.t()) :: {:ok, [Paddle.Class.t()]} | {:error, search_ldap_error} + @spec get(Paddle.Class.t(), Paddle.Filters.t()) :: + {:ok, [Paddle.Class.t()]} | {:error, search_ldap_error} @doc ~S""" Get an entry in the LDAP given a class object. You can specify an optional @@ -422,15 +439,20 @@ defmodule Paddle do memberUid: nil, userPassword: nil}]} """ def get(object, additional_filter \\ nil) when is_map(object) do - fields_filter = object - |> Map.from_struct - |> Enum.filter(fn {_key, value} -> value != nil end) - filter = object - |> Paddle.Class.object_classes - |> Filters.class_filter - |> Filters.merge_filter(fields_filter) - |> Filters.merge_filter(additional_filter) + fields_filter = + object + |> Map.from_struct() + |> Enum.filter(fn {_key, value} -> value != nil end) + + filter = + object + |> Paddle.Class.object_classes() + |> Filters.class_filter() + |> Filters.merge_filter(fields_filter) + |> Filters.merge_filter(additional_filter) + location = Paddle.Class.location(object) + with {:ok, entries} <- GenServer.call(Paddle, {:get, filter, location, :base}) do {:ok, entries @@ -448,8 +470,8 @@ defmodule Paddle do result end - @spec get!(Paddle.Class.t) :: [Paddle.Class.t] - @spec get!(Paddle.Class.t, Paddle.Filters.t) :: [Paddle.Class.t] + @spec get!(Paddle.Class.t()) :: [Paddle.Class.t()] + @spec get!(Paddle.Class.t(), Paddle.Filters.t()) :: [Paddle.Class.t()] @doc ~S""" Same as `get/2` but throws in case of an error. @@ -475,11 +497,10 @@ defmodule Paddle do {:error, :noSuchObject} """ def get_single(kwdn) do - GenServer.call(Paddle, - {:get_single, - Keyword.get(kwdn, :filter), - Keyword.get(kwdn, :base), - :base}) + GenServer.call( + Paddle, + {:get_single, Keyword.get(kwdn, :filter), Keyword.get(kwdn, :base), :base} + ) end # ============ @@ -487,9 +508,13 @@ defmodule Paddle do # ============ @type attributes :: keyword | %{required(binary) => binary} | [{binary, binary}] - @type add_ldap_error :: :undefinedAttributeType | :objectClassViolation | - :invalidAttributeSyntax | :noSuchObject | :insufficientAccessRights | - :entryAlreadyExists + @type add_ldap_error :: + :undefinedAttributeType + | :objectClassViolation + | :invalidAttributeSyntax + | :noSuchObject + | :insufficientAccessRights + | :entryAlreadyExists @spec add(dn, attributes) :: :ok | {:error, add_ldap_error} @@ -512,11 +537,13 @@ defmodule Paddle do a char list as an attribute value. But, you can wrap it in an array like this: `homeDirectory: ['/home/user']` """ - def add(kwdn, attributes), do: - GenServer.call(Paddle, {:add, kwdn, attributes, :base}) + def add(kwdn, attributes), do: GenServer.call(Paddle, {:add, kwdn, attributes, :base}) - @spec add(Paddle.Class.t) :: :ok | {:error, :missing_unique_identifier} | - {:error, :missing_req_attributes, [atom]} | {:error, add_ldap_error} + @spec add(Paddle.Class.t()) :: + :ok + | {:error, :missing_unique_identifier} + | {:error, :missing_req_attributes, [atom]} + | {:error, add_ldap_error} @doc ~S""" Add an entry to the LDAP given a class object. @@ -536,10 +563,9 @@ defmodule Paddle do # == Deleting == # ============== - @type delete_ldap_error :: :noSuchObject | :notAllowedOnNonLeaf | - :insufficientAccessRights + @type delete_ldap_error :: :noSuchObject | :notAllowedOnNonLeaf | :insufficientAccessRights - @spec delete(Paddle.Class.t | dn) :: :ok | {:error, delete_ldap_error} + @spec delete(Paddle.Class.t() | dn) :: :ok | {:error, delete_ldap_error} @doc ~S""" Delete a LDAP entry given a DN or a class object. @@ -567,15 +593,23 @@ defmodule Paddle do # == Modifying == # =============== - @type mod :: {:add, {binary | atom, binary | [binary]}} | {:delete, binary} | - {:replace, {binary | atom, binary | [binary]}} + @type mod :: + {:add, {binary | atom, binary | [binary]}} + | {:delete, binary} + | {:replace, {binary | atom, binary | [binary]}} - @type modify_ldap_error :: :noSuchObject | :undefinedAttributeType | - :namingViolation | :attributeOrValueExists | :invalidAttributeSyntax | - :notAllowedOnRDN | :objectClassViolation | :objectClassModsProhibited | - :insufficientAccessRights + @type modify_ldap_error :: + :noSuchObject + | :undefinedAttributeType + | :namingViolation + | :attributeOrValueExists + | :invalidAttributeSyntax + | :notAllowedOnRDN + | :objectClassViolation + | :objectClassModsProhibited + | :insufficientAccessRights - @spec modify(Paddle.Class.t | dn, [mod]) :: :ok | {:error, modify_ldap_error} + @spec modify(Paddle.Class.t() | dn, [mod]) :: :ok | {:error, modify_ldap_error} @doc ~S""" Modify an LDAP entry given a DN or a class object and a list of @@ -643,17 +677,18 @@ defmodule Paddle do hosts when is_list(hosts) -> Enum.map(hosts, &String.to_charlist/1) end end - def config(:ssl), do: config(:ssl, false) - def config(:ipv6), do: config(:ipv6, false) - def config(:tcpopts), do: config(:tcpopts, []) - def config(:sslopts), do: config(:sslopts, []) - def config(:port), do: config(:port, 389) - def config(:timeout), do: config(:timeout, nil) - def config(:base), do: config(:base, "") |> :binary.bin_to_list - def config(:account_base), do: config(:account_subdn) ++ ',' ++ config(:base) - def config(:account_subdn), do: config(:account_subdn, "ou=People") |> :binary.bin_to_list + + def config(:ssl), do: config(:ssl, false) + def config(:ipv6), do: config(:ipv6, false) + def config(:tcpopts), do: config(:tcpopts, []) + def config(:sslopts), do: config(:sslopts, []) + def config(:port), do: config(:port, 389) + def config(:timeout), do: config(:timeout, nil) + def config(:base), do: config(:base, "") |> :binary.bin_to_list() + def config(:account_base), do: config(:account_subdn) ++ ',' ++ config(:base) + def config(:account_subdn), do: config(:account_subdn, "ou=People") |> :binary.bin_to_list() def config(:account_identifier), do: config(:account_identifier, :uid) - def config(:schema_files), do: config(:schema_files, []) + def config(:schema_files), do: config(:schema_files, []) @spec config(atom, any) :: any @@ -662,8 +697,8 @@ defmodule Paddle do """ def config(key, default), do: Keyword.get(config(), key, default) - @spec ensure_single_result({:ok, [ldap_entry]} | {:error, atom}) - :: {:ok, ldap_entry} | {:error, :noSuchObject} + @spec ensure_single_result({:ok, [ldap_entry]} | {:error, atom}) :: + {:ok, ldap_entry} | {:error, :noSuchObject} defp ensure_single_result({:error, error}) do case error do @@ -678,15 +713,17 @@ defmodule Paddle do @doc false def eldap_log_callback(level, format_string, format_args) do - message = case Application.get_env(:paddle, :filter_passwords, true) do - true -> - :io_lib.format(format_string, format_args) - |> to_string() - |> String.replace(~r/{simple,".*"}/, ~s({simple,"filtered"})) - false -> - :io_lib.format(format_string, format_args) - end - + message = + case Application.get_env(:paddle, :filter_passwords, true) do + true -> + :io_lib.format(format_string, format_args) + |> to_string() + |> String.replace(~r/{simple,".*"}/, ~s({simple,"filtered"})) + + false -> + :io_lib.format(format_string, format_args) + end + case level do # Level 1 seems unused by :eldap 1 -> Logger.info(message) @@ -694,47 +731,48 @@ defmodule Paddle do end end - defp do_connect(opts \\ []) do - ssl = Keyword.get(opts, :ssl, config(:ssl)) - ipv6 = Keyword.get(opts, :ipv6, config(:ipv6)) + defp do_connect(opts) do + ssl = Keyword.get(opts, :ssl, config(:ssl)) + ipv6 = Keyword.get(opts, :ipv6, config(:ipv6)) tcpopts = Keyword.get(opts, :tcpopts, config(:tcpopts)) sslopts = Keyword.get(opts, :sslopts, config(:sslopts)) - host = Keyword.get(opts, :host, config(:host)) - port = Keyword.get(opts, :port, config(:port)) + host = Keyword.get(opts, :host, config(:host)) + port = Keyword.get(opts, :port, config(:port)) timeout = Keyword.get(opts, :timeout, config(:timeout)) - Logger.info("Connecting to ldap#{if ssl, do: "s"}://#{inspect host}:#{port}") + Logger.info("Connecting to ldap#{if ssl, do: "s"}://#{inspect(host)}:#{port}") - tcpopts = if ipv6 do - [:inet6 | tcpopts] - else - tcpopts - end + tcpopts = + if ipv6 do + [:inet6 | tcpopts] + else + tcpopts + end - options = [ssl: ssl, - port: port, - tcpopts: tcpopts, - log: &eldap_log_callback/3] + options = [ssl: ssl, port: port, tcpopts: tcpopts, log: &eldap_log_callback/3] - options = if timeout do - Keyword.put(options, :timeout, timeout) - else - options - end + options = + if timeout do + Keyword.put(options, :timeout, timeout) + else + options + end - options = if ssl do - Keyword.put(options, :sslopts, sslopts) - else - options - end + options = + if ssl do + Keyword.put(options, :sslopts, sslopts) + else + options + end - Logger.debug("Effective :eldap options: #{inspect options}") + Logger.debug("Effective :eldap options: #{inspect(options)}") case :eldap.open(host, options) do {:ok, ldap_conn} -> :eldap.controlling_process(ldap_conn, self()) Logger.info("Connected to LDAP") {:ok, ldap_conn} + {:error, reason} -> Logger.info("Failed to connect to LDAP") {:error, Kernel.to_string(reason)} diff --git a/lib/paddle/application.ex b/lib/paddle/application.ex index 4167c3d..1951659 100644 --- a/lib/paddle/application.ex +++ b/lib/paddle/application.ex @@ -5,8 +5,8 @@ defmodule Paddle.Application do def start(_type, _args) do Supervisor.start_link([Paddle], - strategy: :one_for_one, - name: Paddle.Supervisor) + strategy: :one_for_one, + name: Paddle.Supervisor + ) end - end diff --git a/lib/paddle/attributes.ex b/lib/paddle/attributes.ex index 4eb8ee1..16ffd51 100644 --- a/lib/paddle/attributes.ex +++ b/lib/paddle/attributes.ex @@ -3,7 +3,7 @@ defmodule Paddle.Attributes do Module used internally by Paddle to manipulate / convert LDAP attributes. """ - @spec get(Paddle.Class.t) :: {:ok, map} | {:error, :missing_required_attributes, [atom]} + @spec get(Paddle.Class.t()) :: {:ok, map} | {:error, :missing_required_attributes, [atom]} @doc ~S""" Get the given and the generated attributes of a given class object. @@ -22,12 +22,12 @@ defmodule Paddle.Attributes do given_attributes = Map.from_struct(class_object) generated_attributes = generate_defaults(class_object) - attributes = Map.merge(generated_attributes, given_attributes, - &choose_value/3) + attributes = Map.merge(generated_attributes, given_attributes, &choose_value/3) required_attributes = Paddle.Class.required_attributes(class_object) missing_req_attributes = get_missing_req(attributes, required_attributes) + case missing_req_attributes do [] -> {:ok, attributes} _ -> {:error, :missing_required_attributes, missing_req_attributes} @@ -50,10 +50,9 @@ defmodule Paddle.Attributes do defp get_missing_req(attributes, required_attributes) do attributes - |> Enum.filter_map(fn {attribute, value} -> + |> Enum.filter(fn {attribute, value} -> attribute in required_attributes and value == nil - end, - fn {attribute, _value} -> attribute end) + end) + |> Enum.map(fn {attribute, _value} -> attribute end) end - end diff --git a/lib/paddle/class.ex b/lib/paddle/class.ex index 98a57df..0467582 100644 --- a/lib/paddle/class.ex +++ b/lib/paddle/class.ex @@ -14,7 +14,7 @@ defprotocol Paddle.Class do `Paddle.PosixAccount` and `Paddle.PosixGroup`. """ - @spec unique_identifier(Paddle.Class.t) :: atom + @spec unique_identifier(Paddle.Class.t()) :: atom @doc ~S""" Return the name of the attribute used in the DN to uniquely identify entries. @@ -24,7 +24,7 @@ defprotocol Paddle.Class do """ def unique_identifier(_) - @spec object_classes(Paddle.Class.t) :: binary | [binary] + @spec object_classes(Paddle.Class.t()) :: binary | [binary] @doc ~S""" Must return the class or the list of classes which this "object class" @@ -37,7 +37,7 @@ defprotocol Paddle.Class do """ def object_classes(_) - @spec required_attributes(Paddle.Class.t) :: [atom] + @spec required_attributes(Paddle.Class.t()) :: [atom] @doc ~S""" Return the list of required attributes for this "class" @@ -49,7 +49,7 @@ defprotocol Paddle.Class do """ def required_attributes(_) - @spec location(Paddle.Class.t) :: binary | keyword + @spec location(Paddle.Class.t()) :: binary | keyword @doc ~S""" Return the parent subDN (where to add / get entries of this type). @@ -60,7 +60,7 @@ defprotocol Paddle.Class do """ def location(_) - @spec generators(Paddle.Class.t) :: [{atom, ((Paddle.Class) -> term)}] + @spec generators(Paddle.Class.t()) :: [{atom, (Paddle.Class -> term)}] @doc ~S""" Return a list of attributes to be generated using the given functions. @@ -80,7 +80,6 @@ defprotocol Paddle.Class do Paddle.PosixAccount.get_next_uid(%Paddle.PosixAccount{uid: "myUser", ...} """ def generators(_) - end defmodule Paddle.Class.Helper do @@ -144,12 +143,12 @@ defmodule Paddle.Class.Helper do documentation](#module-manually-describing-the-class)). """ defmacro gen_class(class_name, options) do - fields = Keyword.get(options, :fields) - unique_identifier = Keyword.get(options, :unique_identifier) - object_classes = Keyword.get(options, :object_classes) + fields = Keyword.get(options, :fields) + unique_identifier = Keyword.get(options, :unique_identifier) + object_classes = Keyword.get(options, :object_classes) required_attributes = Keyword.get(options, :required_attributes) - location = Keyword.get(options, :location) - generators = Keyword.get(options, :generators, []) + location = Keyword.get(options, :location) + generators = Keyword.get(options, :generators, []) quote do defmodule unquote(class_name) do @@ -157,11 +156,11 @@ defmodule Paddle.Class.Helper do end defimpl Paddle.Class, for: unquote(class_name) do - def unique_identifier(_), do: unquote(unique_identifier) - def object_classes(_), do: unquote(object_classes) + def unique_identifier(_), do: unquote(unique_identifier) + def object_classes(_), do: unquote(object_classes) def required_attributes(_), do: unquote(required_attributes) - def location(_), do: unquote(location) - def generators(_), do: unquote(generators) + def location(_), do: unquote(location) + def generators(_), do: unquote(generators) end end end @@ -176,16 +175,22 @@ defmodule Paddle.Class.Helper do `Paddle.Class.unique_identifier/1`), and some optional generators (see `Paddle.Class.generators/1`) """ - defmacro gen_class_from_schema(class_name, object_classes, location, unique_identifier \\ nil, generators \\ []) do - {class_name, _bindings} = Code.eval_quoted(class_name, [], __CALLER__) - {object_classes, _bindings} = Code.eval_quoted(object_classes, [], __CALLER__) - {location, _bindings} = Code.eval_quoted(location, [], __CALLER__) + defmacro gen_class_from_schema( + class_name, + object_classes, + location, + unique_identifier \\ nil, + generators \\ [] + ) do + {class_name, _bindings} = Code.eval_quoted(class_name, [], __CALLER__) + {object_classes, _bindings} = Code.eval_quoted(object_classes, [], __CALLER__) + {location, _bindings} = Code.eval_quoted(location, [], __CALLER__) {unique_identifier, _bindings} = Code.eval_quoted(unique_identifier, [], __CALLER__) - {generators, _bindings} = Code.eval_quoted(generators, [], __CALLER__) + {generators, _bindings} = Code.eval_quoted(generators, [], __CALLER__) - fields = Paddle.SchemaParser.attributes(object_classes) + fields = Paddle.SchemaParser.attributes(object_classes) required_attributes = Paddle.SchemaParser.required_attributes(object_classes) - unique_identifier = unique_identifier || hd(required_attributes) + unique_identifier = unique_identifier || hd(required_attributes) quote do defmodule unquote(class_name) do @@ -193,13 +198,12 @@ defmodule Paddle.Class.Helper do end defimpl Paddle.Class, for: unquote(class_name) do - def unique_identifier(_), do: unquote(unique_identifier) - def object_classes(_), do: unquote(object_classes) + def unique_identifier(_), do: unquote(unique_identifier) + def object_classes(_), do: unquote(object_classes) def required_attributes(_), do: unquote(required_attributes) - def location(_), do: unquote(location) - def generators(_), do: unquote(generators) + def location(_), do: unquote(location) + def generators(_), do: unquote(generators) end end end - end diff --git a/lib/paddle/filters.ex b/lib/paddle/filters.ex index 6012a0c..03f2746 100644 --- a/lib/paddle/filters.ex +++ b/lib/paddle/filters.ex @@ -45,17 +45,21 @@ defmodule Paddle.Filters do def construct_filter(filter) when is_tuple(filter), do: filter def construct_filter(nil), do: :eldap.and([]) - def construct_filter(filter) when is_map(filter), do: filter - |> Enum.into([]) - |> construct_filter + def construct_filter(filter) when is_map(filter) do + filter + |> Enum.into([]) + |> construct_filter() + end def construct_filter([{key, value}]) when is_binary(value) do :eldap.equalityMatch('#{key}', '#{value}') end def construct_filter(kwdn) when is_list(kwdn) do - criteria = kwdn - |> Enum.map(fn {key, value} -> :eldap.equalityMatch('#{key}', '#{value}') end) + criteria = + kwdn + |> Enum.map(fn {key, value} -> :eldap.equalityMatch('#{key}', '#{value}') end) + :eldap.and(criteria) end @@ -119,9 +123,8 @@ defmodule Paddle.Filters do def class_filter(classes) when is_list(classes) do classes |> Enum.map(&:eldap.equalityMatch('objectClass', '#{&1}')) - |> :eldap.and + |> :eldap.and() end def class_filter(class), do: :eldap.equalityMatch('objectClass', '#{class}') - end diff --git a/lib/paddle/parsing.ex b/lib/paddle/parsing.ex index b521657..eddc3e4 100644 --- a/lib/paddle/parsing.ex +++ b/lib/paddle/parsing.ex @@ -8,8 +8,6 @@ defmodule Paddle.Parsing do # == 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. @@ -30,33 +28,34 @@ defmodule Paddle.Parsing do reordered and because they can be mistaken for a class object (see `Paddle.Class`). """ - def construct_dn(subdn, base) when is_binary(base) do - construct_dn(subdn, :binary.bin_to_list(base)) - end - + @spec construct_dn(keyword | [{binary, binary}], binary | charlist) :: charlist def construct_dn(map, base \\ '') 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(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 = + 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 + def construct_dn(subdn, base) when is_binary(base), + do: construct_dn(subdn, :binary.bin_to_list(base)) - @spec dn_to_kwlist(charlist | binary) :: [{binary, binary}] + def construct_dn(map, base) when is_list(base), + do: construct_dn(map, '') ++ ',' ++ base @doc ~S""" Tranform an LDAP DN to a keyword list. @@ -70,12 +69,14 @@ defmodule Paddle.Parsing do iex> Paddle.Parsing.dn_to_kwlist("uid=user,ou=People,dc=organisation,dc=org") [{"uid", "user"}, {"ou", "People"}, {"dc", "organisation"}, {"dc", "org"}] """ + @spec dn_to_kwlist(charlist | binary) :: [{binary, binary}] 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 @@ -94,18 +95,20 @@ defmodule Paddle.Parsing do def ldap_escape(''), do: '' def ldap_escape([char | rest]) do - escaped_char = case char do - ?, -> '\\,' - ?# -> '\\#' - ?+ -> '\\+' - ?< -> '\\<' - ?> -> '\\>' - ?; -> '\\;' - ?" -> '\\\"' - ?= -> '\\=' - ?\\ -> '\\\\' - _ -> [char] - end + escaped_char = + case char do + ?, -> '\\,' + ?# -> '\\#' + ?+ -> '\\+' + ?< -> '\\<' + ?> -> '\\>' + ?; -> '\\;' + ?" -> '\\\"' + ?= -> '\\=' + ?\\ -> '\\\\' + _ -> [char] + end + escaped_char ++ ldap_escape(rest) end @@ -119,10 +122,10 @@ defmodule Paddle.Parsing do @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} + {: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. @@ -149,15 +152,14 @@ defmodule Paddle.Parsing 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, entries, []}}, base) do - {:ok, clean_entries(entries, base)} + def clean_eldap_search_results({:ok, tuple}, base) when is_tuple(tuple) do + case elem(tuple, 1) do + [] -> {:error, :noSuchObject} + entries -> {:ok, clean_entries(entries, base)} + end end - @spec entry_to_class_object(Paddle.ldap_entry, Paddle.Class.t) :: Paddle.Class.t + @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. @@ -172,15 +174,16 @@ defmodule Paddle.Parsing do 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(%{}) + 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] + @spec clean_entries([eldap_entry], charlist) :: [Paddle.ldap_entry()] @doc ~S""" Get a binary map representation of several eldap entries. @@ -198,11 +201,12 @@ defmodule Paddle.Parsing do """ def clean_entries(entries, base) do base_length = length(base) + entries - |> Enum.map(&(clean_entry(&1, base_length))) + |> Enum.map(&clean_entry(&1, base_length)) end - @spec clean_entry(eldap_entry, integer) :: Paddle.ldap_entry + @spec clean_entry(eldap_entry, integer) :: Paddle.ldap_entry() @doc ~S""" Get a binary map representation of a single eldap entry. @@ -219,13 +223,16 @@ defmodule Paddle.Parsing do %{"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(%{})) + %{"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) @@ -235,7 +242,7 @@ defmodule Paddle.Parsing do # == Modifications == # =================== - @spec mod_convert(Paddle.mod) :: tuple + @spec mod_convert(Paddle.mod()) :: tuple @doc ~S""" Convert a user-friendly modify operation to an eldap operation. @@ -258,7 +265,7 @@ defmodule Paddle.Parsing do def mod_convert({:add, {field, value}}) do field = '#{field}' - value = list_wrap value + value = list_wrap(value) :eldap.mod_add(field, value) end @@ -269,7 +276,7 @@ defmodule Paddle.Parsing do def mod_convert({:replace, {field, value}}) do field = '#{field}' - value = list_wrap value + value = list_wrap(value) :eldap.mod_replace(field, value) end @@ -308,8 +315,6 @@ defmodule Paddle.Parsing do @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)} + {List.to_string(key), values |> Enum.map(&:binary.list_to_bin/1)} end - end diff --git a/lib/paddle/schema_parser.ex b/lib/paddle/schema_parser.ex index 2d4377c..aab72ac 100644 --- a/lib/paddle/schema_parser.ex +++ b/lib/paddle/schema_parser.ex @@ -11,23 +11,27 @@ defmodule Paddle.SchemaParser do @definitions Paddle.config(:schema_files) |> Enum.flat_map(fn file -> - Logger.info "Loading #{file}" - {:ok, lexed, _num} = file - |> File.read! - |> String.to_charlist - |> :schema_lexer.string + Logger.info("Loading #{file}") + + {:ok, lexed, _num} = + file + |> File.read!() + |> String.to_charlist() + |> :schema_lexer.string() + {:ok, ast} = :schema_parser.parse(lexed) ast end) - @object_definitions Enum.filter(@definitions, - fn {type, _attrs} -> type == :object_class end) - - @attribute_definitions Enum.filter_map(@definitions, - fn {type, _attrs} -> type == :attribute_type end, - fn {:attribute_type, attrs} -> Keyword.get(attrs, :name) end) - ++ [["uid", "userid"]] + @object_definitions Enum.filter( + @definitions, + fn {type, _attrs} -> type == :object_class end + ) + @attribute_definitions for( + {:attribute_type, attrs} <- @definitions, + do: Keyword.get(attrs, :name) + ) ++ [["uid", "userid"]] @spec attributes(binary | [binary]) :: [atom] @@ -49,7 +53,7 @@ defmodule Paddle.SchemaParser do |> Enum.flat_map(&attributes_from/1) |> Enum.map(&replace_alias/1) |> Enum.map(&String.to_atom/1) - |> Enum.uniq + |> Enum.uniq() end defp attributes_from({:object_class, description}) do @@ -75,7 +79,7 @@ defmodule Paddle.SchemaParser do |> Enum.flat_map(&required_attributes_from/1) |> Enum.map(&replace_alias/1) |> Enum.map(&String.to_atom/1) - |> Enum.uniq + |> Enum.uniq() end defp required_attributes_from({:object_class, description}) do @@ -97,26 +101,27 @@ defmodule Paddle.SchemaParser do end defp filter_definitions(definitions, object_classes) when is_list(object_classes) do - object_classes = object_classes - |> Enum.map(fn class -> {class, :notfound} end) - |> Enum.into(%{}) + object_classes = + object_classes + |> Enum.map(fn class -> {class, :notfound} end) + |> Enum.into(%{}) filter_definitions(definitions, object_classes, []) end defp filter_definitions([], object_classes, filtered) when is_map(object_classes) do - not_found = object_classes - |> Enum.filter_map(fn {_class, status} -> status == :notfound end, - fn {class, _status} -> class end) + not_found = for {class, :notfound} <- object_classes, do: class case not_found do [] -> filtered - _ -> raise "Missing object classe(s) definition(s): " <> Enum.join(not_found, ", ") + _ -> raise "Missing object classe(s) definition(s): " <> Enum.join(not_found, ", ") end end - defp filter_definitions([{:object_class, attrs} = class | rest], object_classes, filtered) when is_map(object_classes) do + defp filter_definitions([{:object_class, attrs} = class | rest], object_classes, filtered) + when is_map(object_classes) do name = attrs |> Keyword.get(:name) |> hd + if Map.has_key?(object_classes, name) do if object_classes[name] == :notfound do filter_definitions(rest, Map.put(object_classes, name, :found), [class | filtered]) @@ -134,5 +139,4 @@ defmodule Paddle.SchemaParser do if field in aliases, do: hd(aliases) end) || field end - end diff --git a/mix.exs b/mix.exs index c11e898..54f293a 100644 --- a/mix.exs +++ b/mix.exs @@ -2,26 +2,27 @@ defmodule Paddle.Mixfile do use Mix.Project def project do - [app: :paddle, - version: "0.1.4", - description: "A library simplifying LDAP usage", - elixir: "~> 1.3", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps(), - package: package(), - source_url: "https://github.com/minijackson/paddle", - homepage_url: "https://github.com/minijackson/paddle", - elixirc_paths: elixirc_paths(Mix.env), - docs: [extras: ["README.md"]]] + [ + app: :paddle, + version: "0.1.4", + description: "A library simplifying LDAP usage", + elixir: "~> 1.3", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps(), + package: package(), + source_url: "https://github.com/minijackson/paddle", + homepage_url: "https://github.com/minijackson/paddle", + elixirc_paths: elixirc_paths(Mix.env()), + docs: [extras: ["README.md"]] + ] end # Configuration for the OTP application # # Type "mix help compile.app" for more information def application do - [applications: [:logger, :eldap, :ssl], - mod: {Paddle.Application, []}] + [applications: [:logger, :eldap, :ssl], mod: {Paddle.Application, []}] end # Dependencies can be Hex packages: @@ -34,20 +35,24 @@ defmodule Paddle.Mixfile do # # Type "mix help deps" for more examples and options defp deps do - [{:ex_doc, "~> 0.19", only: :dev, runtime: false}, - {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, - {:credo, "~> 1.2", only: [:dev, :test], runtime: false}, - {:inch_ex, "~> 0.5", only: [:dev, :test], runtime: false}] + [ + {:ex_doc, "~> 0.19", only: :dev, runtime: false}, + {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.2", only: [:dev, :test], runtime: false}, + {:inch_ex, "~> 0.5", only: [:dev, :test], runtime: false} + ] end defp package() do - [name: :paddle, - maintainers: ["Rémi Nicole"], - licenses: ["MIT"], - links: %{"GitHub" => "https://github.com/minijackson/paddle"}] + [ + name: :paddle, + maintainers: ["Rémi Nicole"], + licenses: ["MIT"], + links: %{"GitHub" => "https://github.com/minijackson/paddle"} + ] end defp elixirc_paths(:prod), do: ["lib"] - defp elixirc_paths(:dev), do: elixirc_paths(:prod) + defp elixirc_paths(:dev), do: elixirc_paths(:prod) defp elixirc_paths(:test), do: ["test/support"] ++ elixirc_paths(:dev) end diff --git a/src/schema_parser.yrl b/src/schema_parser.yrl index 9d92e6a..3691b88 100644 --- a/src/schema_parser.yrl +++ b/src/schema_parser.yrl @@ -179,8 +179,8 @@ unquote(Str) -> split({woid, _Line, Str}) -> [list_to_binary(Str)]; split({qdescrs, _Line, Str}) -> - lists:map(fun(Str) -> unquote(Str) end, string:tokens(Str, " \t\n()")); + lists:map(fun(Str0) -> unquote(Str0) end, string:tokens(Str, " \t\n()")); split({oids, _Line, Str}) -> - lists:map(fun(Str) -> list_to_binary(Str) end, string:tokens(Str, "$ \t\n()")). + lists:map(fun(Str0) -> list_to_binary(Str0) end, string:tokens(Str, "$ \t\n()")). kind({Kind, _Line}) -> Kind. diff --git a/test/paddle_test.exs b/test/paddle_test.exs index d7bdaa2..7537c3c 100644 --- a/test/paddle_test.exs +++ b/test/paddle_test.exs @@ -3,10 +3,34 @@ defmodule PaddleTest do doctest Paddle test "class generator helper macro" do - assert Paddle.get!(%MyApp.Room{}) == [%MyApp.Room{cn: ["ùnícödəR°°m"], description: ["The Room where ùnícödə happens 🏰"], roomNumber: ["8"]}, - %MyApp.Room{cn: ["meetingRoom"], description: ["The Room where meetings happens"], roomNumber: ["42"]}] - assert Paddle.get!(%MyApp.Room{cn: "meetingRoom"}) == [%MyApp.Room{cn: ["meetingRoom"], description: ["The Room where meetings happens"], roomNumber: ["42"]}] - assert Paddle.get!(%MyApp.Room{roomNumber: 42}) == [%MyApp.Room{cn: ["meetingRoom"], description: ["The Room where meetings happens"], roomNumber: ["42"]}] + assert Paddle.get!(%MyApp.Room{}) == [ + %MyApp.Room{ + cn: ["ùnícödəR°°m"], + description: ["The Room where ùnícödə happens 🏰"], + roomNumber: ["8"] + }, + %MyApp.Room{ + cn: ["meetingRoom"], + description: ["The Room where meetings happens"], + roomNumber: ["42"] + } + ] + + assert Paddle.get!(%MyApp.Room{cn: "meetingRoom"}) == [ + %MyApp.Room{ + cn: ["meetingRoom"], + description: ["The Room where meetings happens"], + roomNumber: ["42"] + } + ] + + assert Paddle.get!(%MyApp.Room{roomNumber: 42}) == [ + %MyApp.Room{ + cn: ["meetingRoom"], + description: ["The Room where meetings happens"], + roomNumber: ["42"] + } + ] end test "when a connection cannot be open" do diff --git a/test/support/classes.ex b/test/support/classes.ex index a7c2ff9..58be485 100644 --- a/test/support/classes.ex +++ b/test/support/classes.ex @@ -3,24 +3,28 @@ alias Paddle.Class.Helper alias MyApp.Class.Generators -Helper.gen_class_from_schema(MyApp.PosixAccount, - ["posixAccount", "account"], - "ou=People", - :uid, - [uid: &Generators.get_next_uid/1]) - -Helper.gen_class_from_schema(MyApp.PosixGroup, - "posixGroup", - "ou=Group", - :cn, - [gidNumber: &Generators.get_next_gid/1]) +Helper.gen_class_from_schema( + MyApp.PosixAccount, + ["posixAccount", "account"], + "ou=People", + :uid, + uid: &Generators.get_next_uid/1 +) + +Helper.gen_class_from_schema( + MyApp.PosixGroup, + "posixGroup", + "ou=Group", + :cn, + gidNumber: &Generators.get_next_gid/1 +) defmodule MyApp.Class.Generators do @moduledoc ~S""" Class used to aggregate the generators of MyApp's provided object classes. """ - @spec get_next_uid(Paddle.Class.t) :: integer + @spec get_next_uid(Paddle.Class.t()) :: integer @doc ~S""" Get a uid for a new user. @@ -32,10 +36,10 @@ defmodule MyApp.Class.Generators do (Paddle.get!(%MyApp.PosixAccount{}) |> Enum.flat_map(&Map.get(&1, :uidNumber)) |> Enum.map(&String.to_integer/1) - |> Enum.max) + 1 + |> Enum.max()) + 1 end - @spec get_next_gid(Paddle.Class.t) :: integer + @spec get_next_gid(Paddle.Class.t()) :: integer @doc ~S""" Get a gid for a new group. @@ -47,14 +51,14 @@ defmodule MyApp.Class.Generators do (Paddle.get!(%MyApp.PosixGroup{}) |> Enum.flat_map(&Map.get(&1, :gidNumber)) |> Enum.map(&String.to_integer/1) - |> Enum.max) + 1 + |> Enum.max()) + 1 end end Paddle.Class.Helper.gen_class(MyApp.Room, - fields: [:cn, :roomNumber, :description, - :seeAlso, :telephoneNumber], - unique_identifier: :cn, - object_classes: ["room"], - required_attributes: [:commonName], - location: "ou=Rooms") + fields: [:cn, :roomNumber, :description, :seeAlso, :telephoneNumber], + unique_identifier: :cn, + object_classes: ["room"], + required_attributes: [:commonName], + location: "ou=Rooms" +)