Given a piece of Elixir code (as a string, or AST), Formular runs it with Elixir’s Code module under some security limitations.
So far, the limitations are:
No calling module functions;
No calling exit;
No sending messages.
Indeed, the whole library consists of only one thin module, thanks to the power Elixir has already shipped out of the box.
Motivation
Formular was developed to support some dynamic configuration scenarios. For example, in a scene of an online book store, the discount of a book can be dynamically configured as a piece of code, then evaluated by Formular:
iex> discount_formula = ~s"
...> case order do
...> # old books get a big promotion
...> %{book: %{year: year}} when year < 2000 ->
...> 0.5
...>
...> %{book: %{tags: tags}} ->
...> # Elixir books!
...> if ~s{elixir} in tags do
...> 0.9
...> else
...> 1.0
...> end
...>
...> _ ->
...> 1.0
...> end
...> "
...>
...> book_order = %{
...> book: %{
...> title: "Elixir in Action", year: 2019, tags: ["elixir"]
...> }
...> }
...>
...> Formular.eval(discount_formula, [order: book_order])
{:ok, 0.9}
In such a way, the discount calculation code, which changes frequently, is separated away from the stable business flow, and the primary code is probably more generic and flexible.
I’ve been using it in production for a while so I publish it today in case others may find it useful too.
And as soon as you have access to apply/3 (or any of the spawn_*/3 family) then you can run any code you want. In general as soon as you have access to import then you can do anything, and you do not prevent import in any way (it is imported by default as it is part of Kernel.SpecialForms).
If you want something like that, it is better to use any embedded language that is distinct from the Elixir and give it access only to needed primitives. You can take a look on Luerl or Erlog for example.
I suppose it wasn’t meant to be safe, meaning resistant to malicious input, but rather to impose some restrictions on the code that’s changing frequently to limit its impact on the system. Although I have doubts if that’s the correct approach, that’s why I mentioned Sand, that aims to be an actual sandbox.
Thanks for pointing out Luerl and Erlong which are very solid and good references. However, what I want to achieve is to compile the config into Elixir code which can be sent to and used in some Elixir applications.
Ideally, there can be a service with some UI to manage the configuration rules. On update of any rule, the change is synchronized to some services who are interested in the config. The configuration would be compiled into BEAM code so that it can be directly called in the code.
Lua, or Prolog also works in such scenario, but I prefer Elixir because:
a) Elixir has a more friendly syntax IMHO (personal taste?)
b) I think compiling to BEAM code instead of running in a sandbox is more performant. But I haven’t benchmarked it yet. Will try to see how different approaches work.
I built a configuration management system in Elixir years ago but the data format was a little lispy formatted JSON. It worked very well but I think compiling rules to Elixir code would be more fun!
I played with Sand as @mat-hek shared and it does what I wanted. Not implying by the name, under the scene, Sand runs the code with Code.eval_quoted/3 too. Only in a separated process which can be limited in reductions & memory usage. I think that is the right way to go.
Also, I did some benchmarks, and the result was surprising at first glance.
I’m glad to see it is helpful to you too! And I really appreciate that you give it a try.
Please be aware that, as discussed here, Formular is not a safe sandbox right now. The design purpose is more about compiling your configuration into runnable code inside the application. So if the code comes from some untrusted user inputs, it could potentially damage the system. Improving is on the way though.
I don’t get code from user input.
So safety is sorted for us.
The big benefit with Formular for us is that since it’s easy to understand, business can modify the rules.
And that way we can keep the logic as Formular rules and away from the codebase.