How to Develop a Restful Service on top of Freeradius using Elixir/Phoenix

A little about Elixir and Phoenix

Elixir is a functional programming language that runs on top of Erlang. With Elixir, you get modern expressive syntax, concurrency, fault tolerance, performance, and scalability out of the box. As a developer who has used other programming languages mainly OOP, trust me when I say my productivity improved quite a lot with Elixir. Please find more information here.

Phoenix is the go-to web framework for Elixir. It has everything you need to build your next thing.  Unlike other web frameworks where you can easily get lost in the codebase, Phoenix applications have everything in the place where you think they should be. Phoenix is just but a bunch of Elixir modules.  Find more information here.

Our Use Case 

Let’s imagine that company X wants their employees to sign in to their network and has contracted us to build a backend service that runs on top of Freeradius. Freeradius is an open source multi-protocol policy server that is modular, and highly configurable. It can be used to authorize and authenticate users in a network. Let’s imagine that this architecture will use Password Authentication Protocol (PAP) and all we need to implement is the logic to add users to the radius database and the ability to add and remove these users from the network.

We are going to develop this service using Elixir/Phoenix, Ecto, and PostgreSQL. Ecto is a well-built ORM that is very easy to use. We shall use it to generate our migrations, validate external/incoming data, and interact with our tables. Since this is not a “hello world” type of application, I am going to highlight the important parts that will get you started designing a Restful service in Phoenix.  If you want to jump ahead, knock yourself out with the code.

Creating Phoenix Application

If you have not yet installed Elixir (which also requires Erlang to be installed) and Phoenix, please follow the guide in the links shared above. We are going to use Mix to bootstrap our application. Mix has tasks that can help you in your day-to-day development. To get a list of what you can use, try running mix help on your terminal. Okay enough with that and let’s generate our application with the command mix phx.new radiusAPI –no-html –no-assets. Take your cup of coffee and change the directory to radiusApi when it’s done. Inside the lib folder is where we shall spend doing what we love unless we need migrations and we can achieve that in the /priv/repo/migrations folder. Configuration is also important and let’s look at that next.

Configuring our Application

In Phoenix, or any Mix project, all your configurations are usually done in the config folder. Config module takes care of our configuration and you can learn more here. Just know that if you want to configure how your application is compiled, put your config in config/config.exs. If you want to configure the application i.e database for prod, put it in the config/runtime.exs.Usually, we put sensitive information in .env file so let’s do that

export DB_PORT=5432
export DB_USER=radiusapi_dev
export DB_NAME=radiusapi_dev
export DB_PASS=radiusapi_dev
export DB_HOST=localhost

Open the config/dev.exs file and use the System module to get the config values in the environment as illustrated below. By the way, to export these configs, just run source .env on your terminal.

config :radiusApi, RadiusApi.Repo,
  username: System.get_env("DB_USER"),
  password: System.get_env("DB_PASS"),
  hostname: System.get_env("DB_HOST"),
  database: System.get_env("DB_NAME"),
  port: System.get_env("DB_PORT"),
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

Once you are done, make sure to run this mix task on your terminal to create our database if it’s not already mix ecto.create. let’s move on to create our first migration.

Adding Migrations

Almost all web frameworks have a way to keep track of changes made to the database tables. Ecto Migration is the module that will help us manage our migrations. All migrations are kept in /priv/repo/migrations/. Repo in this case is the name of our repository that is found in /lib/radiusApi/repo.ex. Like Phoenix which has mix phx.* tasks, Ecto also comes with helpers that we can run on our terminal. One impressive thing about Ecto is that it will figure out how to rollback your changes without specifying in the down callback. So let’s generate a user table migration with the command mix ecto.gen.migration create_user. Okay, a file is generated. let’s have a look at it.

defmodule RadiusApi.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    
  end
end

Now, let’s create a user table with email (unique), first name, last name, and password (clear text password. I am sure this is not secure)

defmodule RadiusApi.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string, null: false
      add :firstname, :string, null: false
      add :lastname, :string

      # since its going to be Password Authentication Protocol (PAP), this password is going to be in clear text
      add :password, :string, null: false

      timestamps(type: :utc_datetime)
    end

    create unique_index(:users, [:email])
  end
end

use Ecto.Migration on the above code injects the functionality that we need to write our migrations. As you can see, this is how Elixir is expressive. Note that we also created a unique index on the email column and the name <table_column_index>will be automatically generated.

Ecto Schema and Changesets

As you may have noticed, our application inside the lib folder has two folders, radiusApi and radiusApi_web. The former is where all our business logic goes and the latter is where our web-facing logic goes. so let’s focus on our schema first.

Ecto Schema helps us to put a face to our tables. It is Ecto’s API to helps us define what we would normally call a model/entity in other OOP languages. In lib/radiusApi/user, create a file called user.ex and add the following logic and we will go through it

defmodule RadiusApi.Users.User do
  use Ecto.Schema
  import Ecto.Changeset

  @mail_regex ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/

  @permitted [
    :email,
    :firstname,
    :lastname,
    :password
  ]

  @required [
    :email,
    :firstname,
    :password
  ]

  schema "users" do
    field :email, :string
    field :firstname, :string
    field :lastname, :string
    field :password, :string
    timestamps(type: :utc_datetime)
  end

  def changeset(t, attr \\ %{}) do
    t
    |> cast(attr, @permitted)
    |> validate_required(@required)
    |> validate_format(:email, @mail_regex)
    |> unique_constraint(:email)
  end
end

Our Schema is defined in the schema callback and it will automatically create a User struct for us. I also defined allowed and required module attributes explicitly to be used in our changeset. timestamps function will generate for us inserted_at and updated_at columns.

If you are keen enough, you will notice that we have a function definition called changeset. Changeset is what allows us to cast and validate our external parameters. we have also used the changeset function to ensure we have the required fields and that the email is correctly formatted. unique_constraint helps us ensure that we don’t violate the unique constraint and this is also triggered after we hit our database.

Adding Context

Context helps us to group related functionalities together and expose them to our web-facing functionalities. This way, you and I can easily recognize a pattern. Okay continuing with our user functionality, we shall need to have the functionality that operates on the user schema. so inside lib/radiusAPI, create a file called users.ex and define a module like so. For brevity reasons, I have excluded other logic and you can find it in the GitHub code shared above.

defmodule RadiusApi.Users do
  import Ecto.Query
  alias RadiusApi.Repo
  alias RadiusApi.Users.{User}

  def create_user(attr) do
    %User{}
    |> User.changeset(attr)
    |> Repo.insert()
    |> case do
      {:ok, user} ->
        user = Repo.preload(user, :user_devices)
        {:ok, user}

      error ->
        error
    end
  end

  def get_user!(id) do
    Repo.get!(User, id) |> Repo.preload(:user_devices)
  end

  def get_by_email(email) do
    case Repo.get_by(User, email: email) do
      nil -> {:error, "user_not_found"}
      user -> {:ok, Repo.preload(user, :user_devices)}
    end
  end

  def list_users() do
    # password is cleartext because of PAP protocol
    query = from(u in User, select: {u.id, u.email, u.firstname, u.password, u.lastname})

    Repo.all(query)
    |> Enum.map(&from_tuple/1)
  end

  def update(%User{} = user, attr) do
    user
    |> User.changeset(attr)
    |> Repo.update()
  end

  def delete_user(%User{} = user) do
    Repo.delete(user)
  end
  defp from_tuple({id, email, firstname, password, lastname}) do
    %User{
      id: id,
      email: email,
      firstname: firstname,
      lastname: lastname,
      password: password
    }
  end
end

Ecto.Query will get us started writing queries like below. be aware that we shall get a list of tuples that we need to pass using from_tuple/1 function defined above.


def list_users() do
    # password is cleartext because of PAP protocol
    query = from(u in User, select: {u.id, u.email, u.firstname, u.password, u.lastname})

    Repo.all(query)
    |> Enum.map(&from_tuple/1)
  end
User Controller and the View

Once we have our business logic ready, let’s work on the public-facing side of things starting with Controllers and Views. Inside the controller, we can define actions that will be triggered when a request hits our router. So, let’s create the user controller inside lib/radiusApi_web/controllers/ and name it user_controller.ex.

defmodule RadiusApiWeb.UserController do
  use RadiusApiWeb, :controller

  alias RadiusApi.Users
  alias RadiusApi.Users.{User}

  action_fallback RadiusApiWeb.FallbackController

  def create_user(conn, %{"user" => user_params}) do
    with {:ok, %User{} = user} <- Users.create_user(user_params) do
      conn
      |> put_status(:created)
      |> render(:show, user: user)
    end
  end

  def get_user(conn, %{"id" => id}) do
    user = Users.get_user!(id)

    conn
    |> put_status(:ok)
    |> render(:show, user: user)
  end

  def get_users(conn, _params) do
    users = Users.list_users()

    conn
    |> put_status(:ok)
    |> render(:index, users: users)
  end

  def update_user(conn, %{"id" => id, "user" => user_params}) do
    user = Users.get_user!(id)

    with {:ok, user} <- Users.update(user, user_params) do
      conn
      |> put_status(:ok)
      |> render(:show, user: user)
    end
  end

  def delete_user(conn, %{"id" => id}) do
    user = Users.get_user!(id)

    with {:ok, %User{}} <- Users.delete_user(user) do
      conn
      |> send_resp(:no_content, "")
    end
  end

end

use RadiusApiWeb, :controller import some of the useful modules that we need. each of our actions takes two arguments. first, being the conn struct that has all the information about the incoming request. the second argument is the parameters that are sent together with the request. All functions in the controller must return %Plug.Conn{}.

action_fallback RadiusApiWeb.FallbackController as the name suggests is the fallback for when nothing matches in this controller. It’s just a Plug with functionality that knows how to return a valid %Plug.Conn{}. You can use action_fallback to standardize your response in different formats across the entire application.

By default, phoenix expects us to have a view inside the controllers’ folder called user_json.ex. This is the module that will be triggered by the render function. Since our application returns a JSON formatted response, the name of the file as you guessed it is named user_json.ex. You can change the view to be used in your response like so put_view(json: <YOUR VIEW>). anyway, let’s create our user view.

defmodule RadiusApiWeb.UserJSON do
  alias RadiusApi.Users.{User}

  def index(%{users: users}) do
    %{data: for(user <- users, do: data(user))}
  end

  def show(%{user: %User{} = user}) do
    %{data: data(user)}
  end

  defp data(%User{} = user) do
    %{
      id: user.id,
      email: user.email,
      firstname: user.firstname,
      lastname: user.lastname,
      password: user.password
    }
  end
end
Adding Routes

In Phoenix, Routes are the main hub. They match HTTP requests to our controller actions. Routes define Scopes, Pipelines, and Routes. Scopes helps to group our routes under a common path prefix like /admin. Pipelines are just a group of Plugs that can be attached to a specific scope. all our routes are defined inside lib/radiusApi_web/routes.ex file. In our case, we have a pipeline called :api that accept JSON as the content type. We also defined a scope called API and attached our :api pipeline to it. Now inside this scope, let’s define our routes like so

defmodule RadiusApiWeb.Router do
  use RadiusApiWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/api", RadiusApiWeb do
    pipe_through :api
    post "/users", UserController, :create_user
    get "/users", UserController, :get_users
    get "/users/:id", UserController, :get_user
    put "/users/:id", UserController, :update_user
    delete "/users/:id", UserController, :delete_user
  end

  # Enable LiveDashboard and Swoosh mailbox preview in development
  if Application.compile_env(:radiusApi, :dev_routes) do
    # If you want to use the LiveDashboard in production, you should put
    # it behind authentication and allow only admins to access it.
    # If your application does not have an admins-only section yet,
    # you can use Plug.BasicAuth to set up some basic authentication
    # as long as you are also using SSL (which you should anyway).
    import Phoenix.LiveDashboard.Router

    scope "/dev" do
      pipe_through [:fetch_session, :protect_from_forgery]

      live_dashboard "/dashboard", metrics: RadiusApiWeb.Telemetry
      forward "/mailbox", Plug.Swoosh.MailboxPreview
    end
  end
end

If you are still following along, we can start the application with mix phx.server. One interesting thing about Elixir/Phoenix is that you can also start the application inside the interactive session called iex like so iex -S mix phx.server. get the Postman collection from here.

Conclusion

We have not covered other concepts such as Application and how our service is supervised but you can read more here or check it in our codebase /lib/radiusApi/application.ex. All in all, writing an Elixir application is just a bliss.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top