How to get name of 'calling' module and/or function?

Currently I am writing a small macro which adds authorization functionality to a function. Rewrite ‘def’ to ‘defprot’ and add the rules to an Authorization module. Too easy it is.

defmodule One do
defprot bar(), do: :result
end

defmodule Two do
def foo(), do: One.bar()
end

In order to write nice debug information and add the possibility to create enforced boundaries between (context) modules, I need the names of the ‘calling’ Function and Module in a macro. Saving them as an environment variable and retrieving them in the macro does work while developing but A: ain’t nice and B: would be useless in case of concurrency.

The output should become:
iex > Two.foo()
[debug] "Two.foo() requested authorization to access One.bar()"

Is there a good way to retrieve the information of the calling Module (Two) and calling Function (foo) while in One.bar()?

3 Likes

For Macro’s you can access the callers environment inside the __CALLER__ argument (implicitly passed in to every macro call). :slight_smile:

It’s just a normal Macro.Env struct.

1 Like

__CALLER__ returns the current calling environment as a Macro.Env struct; a struct that holds compile time environment information. That’s why you can’t use it in a quote block.

However, I need run time information. When function Two.foo/1 is calling function One.bar/1 as in the example, the caller should be Two.foo/1 (info might be split in callingMod en callingFunc)

2 Likes

A function should not care from where it is called. It should return the same regardless the caller.

If though you really have to, you could do it similar how logger injects metadata.

Use a macro which takes the arguments defined by your API, injects code to collect metadata and then delegates to a function that takes the actual arguments AND the collected metadata.

3 Likes

You can use Process.info(self(), :current_stacktrace) to get the current stacktrace (and the calling function should be there), but this is generally considered a debugging utility, not something to be used in production.

Additionally, be aware that tail calls don’t produce stack entries, so in a code like this:

def foo(), do: bar()
def bar(), do: Process.info(self(), :current_stacktrace)

The stacktrace information won’t include foo().

9 Likes

Figured it out myself, but michalmuska was right. Code became:

{callingMod, callingFunc, callingFuncArity, [file: _file, line: _line]} =
      Process.info(self(), :current_stacktrace) |> elem(1) |> Enum.fetch!(2)

Now I need to handle the ‘no stack entry’ situation, somehow :slight_smile:

@NobbZ Will have a look at the proposal. Thanks for the input.

I’m exceptionally curious, “what” are you trying to accomplish? o.O

2 Likes

An insane experiment to build a non plug-based authorization code which can easily applied on existing modules, functions, con(n/text) and even arguments. In Phoenix applications authorization is quite easy as Phoenix uses conventions and does normalize a lot to (conn, object map, current_user). Custom applications don’t have such thing. I doubt I will even succeed, but it’s worth to spend a week of my free time on as I learn a lot :slight_smile:

Had a working POC which needed environment variables to work; which would make it useless. If I can take those out, it might actually be a nice solution to secure an otherwise insecure codebase without much rewrites.

  @doc """
  Replace a function definition, adding a call to the authorization function.
  Example:
      defprot function(arg1, arg2), do: IO.inspect({arg1,arg2})
      function(1,2)

  will call authorize with this parameters

      authorizate(__MODULE__, :function, [arg1: 1, arg2: 2], nil)

  """

If there is a solid way to track which Module and Function did the request, it will also be possible to pattern match at those. Allowing (for example) to mimic defp (just because we can…) or reject all calls to functions which write to the database unless the authorization rules allow them to.

Edit: If I ever find a good solution, I will try to have it added to https://github.com/arjan/decorator, as that lib is doing 90% of what I wrote already. Now only those few last bits…

Edit2: Gotta get some sleep…sigh

2 Likes

Status update:

It is possible, yet it has so many side effects it will never see an open repository for too many good reasons.

iex(auth@127.0.0.1)1> Example.test3
Example.test3() was called when no origin was set. Are you using IEX?
Example.test1() was called by an anonymous function with was called by Example.test3()
Example.test0() was called by Example.test1()
[info] Example.test0 requested authorization to access Example.test/3
[info] Access granted
#PID<0.421.0>
iex(auth@127.0.0.1)2>

Conclusion: Meta programming and message passing between processes rock. Never underestimate their power, but use them wisely.

Read before complains

I did not bother to print arity instead of () anymore as the experiment has ended.

1 Like

Hah, interesting though. ^.^

I’ll clean up the POC one day and request some additional functionality in Elixir to make it possible without a zillion hacks. Now I have a demo to demonstrate the concept :slight_smile:

I cleaned up the code quite a bit, might even try to use it to protect my latest project just to find all the flaws. Here are some details:

def() is overridden so it sends and receives information about the current Module, Fuction and Arity (m/f/a) and wraps the ‘real body’ in a Task so it has it’s own process with inbox. Each function called in the wrapped function will receive a message with the m/f/a that called the function.

Knowing the current m/f/a and the calling m/f/a, a map is created and passed to the authorization function which you write yourself. As it’s a map, it’s easy to do pattern matching! The examples don’t show it, but you are able to protect context (Module) borders, restrict calls to "update_" <> _ for users (for every function of the Module) or giving admins a wildcard with def authorize(%{args: %{context: %{role: :admin}}}), do: :ok

Removed: If a function calls itself (see: get_post/2) this is detected and authorization will be skipped.
Added: Sounds like a rule to me! def authorize(%{mod: mod, func: func, arity: arity, calling_mod: mod, calling_func: func, calling_arity: arity}), do: :ok does the job as expected. Removed the loop detection.

Authorization map:

 auth_map = %{
          calling_mod: calling_mod,
          calling_func: calling_func,
          calling_arity: calling_arity,
          mod: current_mod,
          func: current_func,
          arity: current_arity,
          args: current_args
        }

The rules:

defmodule Main.Authorizer do

  def authorize(auth_map \\ %{})

  # update post rules
  def authorize(%{func: "update_post", args: %{context: %{role: :admin}}}), do: :ok

  def authorize(%{func: "update_post", args: args}) do
    case Main.get_post(args[:id], args[:context]) do
      :unauthorized -> :unauthorized
      post -> post[:author_id] == args[:context][:user_id]  && {:ok, post} || :unauthorized
    end
  end

  # get post rules
  def authorize(%{func: "get_post", args: %{context: %{role: :admin}}}), do: :ok

  def authorize(%{func: "get_post", args: args}) do
    post = Main.get_post(args[:id], args[:context])
    post[:author_id] == args[:context][:user_id]  && {:ok, post} || :unauthorized
  end

  # sink
  def authorize(_), do: :unauthorized
end

The result (User 124 is author of Post 2):

iex(auth@127.0.0.1)1> Main.update_post(2, %{content: "Updated content"}, %{user_id: 124, role: :user})
[info] Main.get_post/2 requested authorization to access Main.get_post/2. [SKIPPED AUTH]
[info] Main.update_post/3 requested authorization to access Main.get_post/2. [ACCESS GRANTED]
[info] An unknown function requested authorization to access Main.update_post/3. Are you using IEX?. [ACCESS GRANTED]
{:ok, %{content: "Updated content"}}
iex(auth@127.0.0.1)2> Main.update_post(2, %{content: "Updated content"}, %{user_id: 123, role: :user})
[info] Main.get_post/2 requested authorization to access Main.get_post/2. [SKIPPED AUTH]
[info] Main.update_post/3 requested authorization to access Main.get_post/2. [ACCESS DENIED]
[info] An unknown function requested authorization to access Main.update_post/3. Are you using IEX?. [ACCESS DENIED]
:unauthorized
iex(auth@127.0.0.1)3> Main.update_post(2, %{content: "Updated content"}, %{user_id: 123, role: :admin})
[info] An unknown function requested authorization to access Main.update_post/3. Are you using IEX?. [ACCESS GRANTED]
{:ok, %{content: "Updated content"}}
iex(auth@127.0.0.1)4>
1 Like