BartOtten
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
enddefmodule 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()?
Marked As Solved
BartOtten
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>
Also Liked
michalmuskala
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().
NobbZ
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.
BartOtten
__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)
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









