07/21/2016 Latest Update: Previous Post in this series _Last Updated At: 07/21/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 2, Authorization Current Versions As of the time of writing this, the current versions of our applications are: : v1.3.1 Elixir v1.2.0 Phoenix: v2.0.2 Ecto: v2.5.2 Comeonin: If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly. Where We Left Off When we last left off, we finished associating our Posts with Users, and began the process of properly restricting access to posts unless we had valid users (and were the user the created the post in the first place), but what if we want to have multiple users? We’ll want to keep things in check with sane user rules so that we do not end up in a scenarios where some rogue user goes off and deletes everyone’s account or posts across the board. We’ll solve this with a fairly standard solution: creating roles. Creating Roles We’ll start off by running the following command in our terminal: $ mix phoenix.gen.model Role roles name:string admin:boolean To which we should expect to see something similar to the following output: * creating web/models/role.ex* creating test/models/role_test.exs* creating priv/repo/migrations/20160721151158_create_role.exs Remember to update your repository by running migrations: $ mix ecto.migrate We’ll follow the script’s advice and run immediately. Assuming our DB is already setup properly, we should see output similar to the following: mix ecto.migrate Compiling 21 files (.ex)Generated pxblog app 11:12:04.736 [info] == Running Pxblog.Repo.Migrations.CreateRole.change/0 forward 11:12:04.736 [info] create table roles 11:12:04.742 [info] == Migrated in 0.0s We’ll also run mix test to sanity check that our new model addition has not disrupted any other tests. If everything is all green, then we’re all set to move on to modifying our User model to have associated an associated Role. Adding the Roles Associations The general design I’m following for this particular feature implementation is that each User has one Role, and that each Role has multiple Users, so we’ll modify the file to reflect this: web/models/user.ex In the schema “users” do section, we’ll add the following line: belongs_to :role, Pxblog.Role In this case, we’re going to place the role_id foreign key on the Users table, so we want to say that a User “belongs to” a Role. We’ll also open up and change the schema “roles” do section by adding the following line: web/models/role.ex has_many :users, Pxblog.User We’ll then run mix test again, but we should be expecting many failures. We told Ecto that our users table had a relationship to the roles table but we never defined that in our database, so we’re going to have to modify our users table to hold a reference to a role_id. $ mix ecto.gen.migration add_role_id_to_users Compiling 5 files (.ex)* creating priv/repo/migrations* creating priv/repo/migrations/20160721184919_add_role_id_to_users.exs Let’s open up the migration file that it created. By default, it’ll contain: defmodule Pxblog.Repo.Migrations.AddRoleIdToUsers douse Ecto.Migration def change doendend We need to add a few things. The first thing is that we need to alter the users table to add the reference to roles, so we’ll do this with the following code: alter table(:users) doadd :role_id, references(:roles)end And we should add an index to role_id, so we’ll add the following line: create index(:users, [:role_id]) Finally, we’ll run again, and should see everything migrate successfully! If we run our tests now, everything should be green again! Unfortunately, our tests are not quite perfect. For one thing, we never modified our tests for the Post/User models to make sure that a Post, for example, must have a User defined. Similarly, we don’t want to be able to create a User when they don’t have a Role. In , we’ll change the changeset function to read as follows (note the two additions of ): mix ecto.migrate web/models/user.ex :role_id def changeset(struct, params \\ %{}) dostruct|> cast(params, [:username, :email, :password, :password_confirmation, :role_id])|> validate_required([:username, :email, :password, :password_confirmation, :role_id])|> hash_passwordend Creating a Test Helper Running our tests now will result in a lot of failures, but that’s okay! We’re going to be doing a lot of work to clean up our tests, and one of the things we’re going to need is some sort of test helper to avoid us having to write the same code over and over. We’re going to create a new file to help us build out these models. Create and populate it with the following code: test/support/test_helper.ex defmodule Pxblog.TestHelper doalias Pxblog.Repoalias Pxblog.Useralias Pxblog.Rolealias Pxblog.Post import Ecto, only: [build_assoc: 2] def create_role(%{name: name, admin: admin}) doRole.changeset(%Role{}, %{name: name, admin: admin})|> Repo.insertend def create_user(role, %{email: email, username: username, password: password, password_confirmation: password_confirmation}) dorole|> build_assoc(:users)|> User.changeset(%{email: email, username: username, password: password, password_confirmation: password_confirmation})|> Repo.insertend def create_post(user, %{title: title, body: body}) douser|> build_assoc(:posts)|> Post.changeset(%{title: title, body: body})|> Repo.insertendend Let’s talk about what that file is doing before we move on with fixing our tests. The first thing to note is where we’ve placed the file: , where we can place any modules we want to make available to our test suite at large. We’ll still need to alias references to these in each of the files, but that’s okay! test/support We alias out our Repo, User, Role, and Post modules first so we can access them with shorter syntax and we also so we can access the method for building associations. import Ecto build_assoc In create_role, we expect a map that includes a role name and whether it’s admin or not. We’re using Repo.insert here which means we’ll be returning the standard response on successful insertions. Otherwise, it’s just a simple insert of a Role changeset. {:ok, model} In , we take as our first argument the role we want to use and and as the second argument a map of parameters to use to create our user. We start with our role and then pipe that into our build function, creating a User model (since we specified :users as the association), which then gets piped into User.changeset with the parameters mentioned previously. The end result of that gets piped into Repo.insert(), and we’re done! create_user While being kind of complicated to explain, we end up with super readable and super understandable code. Take a role, build an associated user, prep it for insertion into the database, and then insert it! For we do the same thing, except with a post and user instead of a user and role! create_post Fixing Our Tests We’ll start by fixing up . The first thing we need to do is add to the top of our module definition so we can use those handy helpers we created earlier. Next, we’re going to create a setup block before our tests to reuse a role. test/models/user_test.exs alias Pxblog.TestHelper setup do{:ok, role} = TestHelper.create_role(%{name: "user", admin: false}){:ok, role: role}end And then in our first test, we’re going to change it around to pattern match against the role key from our setup block. We’re going to save ourselves a little bit of typing whenever we want this role included, so we’re going to write a helper function and modify the test: defp valid_attrs(role) doMap.put(@valid_attrs, :role_id, role.id)end test "changeset with valid attributes", %{role: role} dochangeset = User.changeset(%User{}, valid_attrs(role))assert changeset.valid?end To recap, we pattern match on the role key coming from our setup block, and then we’re modifying the key to include a valid role id in our helper method! When we modify this spec and run it, we should now be back to green specs for ! valid_attrs test/models/user___test.exs Next, open up and we’ll use the same lessons to get this file passing again. We’ll add an statement at the top of our controller file, as well as an statement, and add setup code to create a role and return that out with conn. test/controllers/user_controller___test.exs alias Pxblog.Role alias Pxblog.TestHelper setup do{:ok, user_role} = TestHelper.create_role(%{name: "user", admin: false}){:ok, admin_role} = TestHelper.create_role(%{name: "admin", admin: true}){:ok, conn: build_conn(), user_role: user_role, admin_role: admin_role}end We’ll add a helper function called that takes in a role as an argument and returns out a new map with role_id set as well. valid_create_attrs defp valid_create_attrs(role) doMap.put(@valid_create_attrs, :role_id, role.id)end Finally, we’ll modify our create and update actions to use this new helper and pattern match on the user_role value from our map. test "creates resource and redirects when data is valid", %{conn: conn, user_role: user_role} doconn = post conn, user_path(conn, :create), user: valid_create_attrs(user_role)assert redirected_to(conn) == user_path(conn, :index)assert Repo.get_by(User, @valid_attrs)end test "updates chosen resource and redirects when data is valid", %{conn: conn, user_role: user_role} douser = Repo.insert! %User{}conn = put conn, user_path(conn, :update, user), user: valid_create_attrs(user_role)assert redirected_to(conn) == user_path(conn, :show, user)assert Repo.get_by(User, @valid_attrs)end Our user controller tests should now all be green! Running mix test is still going to give us failures, sadly. Fixing the Post Controller Tests In our Posts Controller, we ended up creating a lot of helper functions to facilitate our building posts with users, so we need to modify those helpers and add the concept of roles so that we can create a valid user. We’ll start by adding a reference to up at the top of our post controller. In : Pxblog.Role test/controllers/post_controller_test.exs alias Pxblog.Rolealias Pxblog.TestHelper Then we’ll create our setup block, deviating slightly from what we’ve done in previous setup blocks. setup do{:ok, role} = TestHelper.create_role(%{name: "User Role", admin: false}){:ok, user} = TestHelper.create_user(role, %{email: "test@test.com", username: "testuser", password: "test", password_confirmation: "test"}){:ok, post} = TestHelper.create_post(user, %{title: "Test Post", body: "Test Body"})conn = build_conn() |> login_user(user){:ok, conn: conn, user: user, role: role, post: post}end The first thing we need to do is create a role, and just a standard non-admin role will do just fine here. In the next line, we’ll also create a user using that role. Next, we’ll create a post for the user. We’ve already covered the login piece, so we’ll skip that. Finally, we return out all of the new models we created to allow each test to pattern match as they need to. And we have one test that we’ll modify as well to get everything green. Our “redirects when trying to edit a post for a different user” test fails because it attempts to create a second user on the fly with no concept of a role. We’ll change it around ever so slightly: test "redirects when trying to edit a post for a different user", %{conn: conn, user: user, role: role, post: post} do{:ok, other_user} = TestHelper.create_user(role, %{email: "test2@test.com", username: "test2", password: "test", password_confirmation: "test"})conn = get conn, user_post_path(conn, :edit, other_user, post)assert get_flash(conn, :error) == "You are not authorized to modify that post!"assert redirected_to(conn) == page_path(conn, :index)assert conn.haltedend So here we’ve added the role: role bit to our test definition to pattern match on the role key, and changed the other_user creation bit to instead use TestHelper and reference the role we pattern matched on. We have a quick opportunity to refactor since we helpfully included a post object from our TestHelper as one of the values we can pattern match. Everywhere we previously called we can drop and instead pattern match on our post object. The full file after all of our modifications should be: build_post defmodule Pxblog.PostControllerTest douse Pxblog.ConnCase alias Pxblog.Postalias Pxblog.TestHelper @valid_attrs %{body: "some content", title: "some content"}@invalid_attrs %{} setup do{:ok, role} = TestHelper.create_role(%{name: "User Role", admin: false}){:ok, user} = TestHelper.create_user(role, %{email: "test@test.com", username: "testuser", password: "test", password_confirmation: "test"}){:ok, post} = TestHelper.create_post(user, %{title: "Test Post", body: "Test Body"})conn = build_conn() |> login_user(user){:ok, conn: conn, user: user, role: role, post: post}end defp login_user(conn, user) dopost conn, session_path(conn, :create), user: %{username: user.username, password: user.password}end test "lists all entries on index", %{conn: conn, user: user} doconn = get conn, user_post_path(conn, :index, user)assert html_response(conn, 200) =~ "Listing posts"end test "renders form for new resources", %{conn: conn, user: user} doconn = get conn, user_post_path(conn, :new, user)assert html_response(conn, 200) =~ "New post"end test "creates resource and redirects when data is valid", %{conn: conn, user: user} doconn = post conn, user_post_path(conn, :create, user), post: @valid_attrsassert redirected_to(conn) == user_post_path(conn, :index, user)assert Repo.get_by(assoc(user, :posts), @valid_attrs)end test "does not create resource and renders errors when data is invalid", %{conn: conn, user: user} doconn = post conn, user_post_path(conn, :create, user), post: @invalid_attrsassert html_response(conn, 200) =~ "New post"end test "shows chosen resource", %{conn: conn, user: user, post: post} doconn = get conn, user_post_path(conn, :show, user, post)assert html_response(conn, 200) =~ "Show post"end test "renders page not found when id is nonexistent", %{conn: conn, user: user} doassert_error_sent 404, fn ->get conn, user_post_path(conn, :show, user, -1)endend test "renders form for editing chosen resource", %{conn: conn, user: user, post: post} doconn = get conn, user_post_path(conn, :edit, user, post)assert html_response(conn, 200) =~ "Edit post"end test "updates chosen resource and redirects when data is valid", %{conn: conn, user: user, post: post} doconn = put conn, user_post_path(conn, :update, user, post), post: @valid_attrsassert redirected_to(conn) == user_post_path(conn, :show, user, post)assert Repo.get_by(Post, @valid_attrs)end test "does not update chosen resource and renders errors when data is invalid", %{conn: conn, user: user, post: post} doconn = put conn, user_post_path(conn, :update, user, post), post: %{"body" => nil}assert html_response(conn, 200) =~ "Edit post"end test "deletes chosen resource", %{conn: conn, user: user, post: post} doconn = delete conn, user_post_path(conn, :delete, user, post)assert redirected_to(conn) == user_post_path(conn, :index, user)refute Repo.get(Post, post.id)end test "redirects when the specified user does not exist", %{conn: conn} doconn = get conn, user_post_path(conn, :index, -1)assert get_flash(conn, :error) == "Invalid user!"assert redirected_to(conn) == page_path(conn, :index)assert conn.haltedend test "redirects when trying to edit a post for a different user", %{conn: conn, role: role, post: post} do{:ok, other_user} = TestHelper.create_user(role, %{email: "test2@test.com", username: "test2", password: "test", password_confirmation: "test"})conn = get conn, user_post_path(conn, :edit, other_user, post)assert get_flash(conn, :error) == "You are not authorized to modify that post!"assert redirected_to(conn) == page_path(conn, :index)assert conn.haltedendend Fixing the Session Controller Tests has some failing tests as well since we’ve not updated it to use our TestHelper. We’ll add a aliases at the top and modify the setup block, as we have elsewhere: test/controllers/session_controller_test.exs defmodule Pxblog.SessionControllerTest douse Pxblog.ConnCase alias Pxblog.Useralias Pxblog.TestHelper setup do{:ok, role} = TestHelper.create_role(%{name: "User", admin: false}){:ok, _user} = TestHelper.create_user(role, %{username: "test", password: "test", password_confirmation: "test", email: "test@test.com"}){:ok, conn: build_conn()}end This should be sufficient to make these tests pass! Hooray! Fixing the Rest of Our Tests We still have two failing tests. Let’s get those green! test current user returns the user in the session (Pxblog.LayoutViewTest)test/views/layout_view_test.exs:13Expected truthy, got nilcode: LayoutView.current_user(conn)stacktrace:test/views/layout_view_test.exs:15 test current user returns nothing if there is no user in the session (Pxblog.LayoutViewTest)test/views/layout_view_test.exs:18** (ArgumentError) cannot convert nil to paramstacktrace:(phoenix) lib/phoenix/param.ex:67: Phoenix.Param.Atom.to_param/1(pxblog) web/router.ex:1: Pxblog.Router.Helpers.session_path/4test/views/layout_view_test.exs:20 Open up , and up at the top we see a User getting created without a Role! In our setup block, we’re also not passing our created User along, so we have to look it up over and over again! Gross! We’re going to refactor the whole file: test/views/layout_view_test.exs defmodule Pxblog.LayoutViewTest douse Pxblog.ConnCase, async: true alias Pxblog.LayoutViewalias Pxblog.TestHelper setup do{:ok, role} = TestHelper.create_role(%{name: "User Role", admin: false}){:ok, user} = TestHelper.create_user(role, %{email: " ", username: "testuser", password: "test", password_confirmation: "test"}){:ok, conn: build_conn(), user: user}end test@test.com test "current user returns the user in the session", %{conn: conn, user: user} doconn = post conn, session_path(conn, :create), user: %{username: user.username, password: user.password}assert LayoutView.current_user(conn)end test "current user returns nothing if there is no user in the session", %{conn: conn, user: user} doconn = delete conn, session_path(conn, :delete, user)refute LayoutView.current_user(conn)endend We add an alias for our Role model, create a valid Role, create a valid User with that role, and then return out user with the conn. Finally, in both of our test methods, we pattern match on user so that we can reuse that model. Now, run mix test and… Everything is green! But we ARE getting a couple of warnings when running our tests (since we made everything so lovely and clean). test/controllers/post_controller_test.exs:20: warning: function create_user/0 is unusedtest/views/layout_view_test.exs:6: warning: unused alias Roletest/views/layout_view_test.exs:5: warning: unused alias Usertest/controllers/user_controller_test.exs:5: warning: unused alias Roletest/controllers/post_controller_test.exs:102: warning: variable user is unusedtest/controllers/post_controller_test.exs:6: warning: unused alias Role Just go in to each of those files and remove the offending aliases and functions since we don’t need them anymore! $ mix test......................................... Finished in 0.4 seconds41 tests, 0 failures Randomized with seed 588307 Creating an Admin Seed Eventually we’ll be restricting new user creation to admins only. For us to do so, however, means that we’ll be in a weird catch-22 state where we have no members or admins, thus meaning we cannot create members or admins, and so on. We’ll remedy this by providing a seed for a default admin user. Open up and insert the following code: priv/repo/seeds.exs alias Pxblog.Repoalias Pxblog.Rolealias Pxblog.User role = %Role{}|> Role.changeset(%{name: "Admin Role", admin: true})|> Repo.insert! admin = %User{}|> User.changeset(%{username: "admin", email: "admin@test.com", password: "test", password_confirmation: "test", role_id: role.id})|> Repo.insert! And then we’ll run our seed file by invoking the following command: $ mix run priv/repo/seeds.exs Coming Up Next Now that we have our models set up and ready for dealing with our roles and have our tests back to green, we need to start modifying the functionality in our controllers to restrict certain operations unless you’re the appropriate user or an admin. In the next post we’ll explore how best to implement this functionality, how to add a helper module, and of course, we’ll keep our tests green! Next post in this series _Latest Update: 01/26/2016_medium.com Writing a Blog Engine in Phoenix and Elixir: Part 4, Adding Roles to our Controllers Check out my new book! Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development: _Learn to build a high-performance functional prototype of a voting web application from scratch using Elixir and…_www.packtpub.com Phoenix Web Development | PACKT Books I’m really excited to finally be bringing this project to the world! It’s written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs! is how hackers start their afternoons. We’re a part of the family. We are now and happy to opportunities. Hacker Noon @AMI accepting submissions discuss advertising & sponsorship To learn more, , , or simply, read our about page like/message us on Facebook tweet/DM @HackerNoon. If you enjoyed this story, we recommend reading our and . Until next time, don’t take the realities of the world for granted! latest tech stories trending tech stories