Lab 11: BandBlitz
Due Date: April 18
- Teach students how to do basic authentication with Rails
- Teach students how to authorize different users to take different actions in the system
- Reinforce previous lessons in rapidly building apps
Due Date: April 18
This lab will serve as an introduction to working with sessions, authentication, and authorization using the Ruby on Rails framework.
git clone https://github.com/67272-App-Design-Dev/lab10-bandblitz-starter.git
Once you get the code, run
bundle install to get the gems such as CanCanCan we will need for this lab.
We want to add authorization, but we must first begin by adding authentication. To do this, create a user model with the following attributes:
rails generate model for now; some user views you will need are already included in starter files.) In the migration set the default value of
role to "member" and the default value of
active to true. Run
rails db:migrate to capture these changes.
User model, create a relationship to
Band (and likewise from
User). Note a band
has_many users and a user
belongs_to a band.
We also want to use Rails' built-in password management, so add the line
has_secure_password to your User model as well. This will create the password-digest, but you will need the bcrypt gem for this to work (make sure it's in your
Gemfile). Add appropriate validation to this model as well as a name method called
proper_name which concatenates the user's first and last names. For validations consider that
As an option, you can also add the following class method to handle logging in via email and use this method later in the sessions_controller (this was demoed in class last week and we'll point out where it would go later in the lab):
def self.authenticate(email,password) find_by_email(email).try(:authenticate, password) end
Quick question: you are saving your work to git, right?
current_user, which we will draw from the session hash (if it is saved... will do that in a moment). We also want to make this a helper method so that our views can access it as well. We will created a
logged_in?method which simply tells us if you are logged in (true if you have a user_id in session hash, i.e., a current_user). Finally, we will have a method called
check_loginthat we can use as an additional before_action in other controllers. The code would be as follows:
private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user def logged_in? current_user end helper_method :logged_in? def check_login redirect_to login_url, alert: "You need to log in to view this page." if current_user.nil? end
Now that we have a
check_login method in ApplicationController, every other controller will also have it because they inherit from ApplicationController.
To use this method set up an additional
before_action to require
check_login be run before any action in the GenresController, and before all actions except index and show in the BandsController. See the Rails Guide for more information on filters if you are unsure of how to do this.
We need to set up a UsersController and it will be much like our standard controllers with the following exceptions:
a) We only need new, edit, create, and update actions this simple app (you can add more if you like, but will also need to add views)
update should get initial object from
current_user method, not an id parameter passed in
c) When a new user is saved during the create method, the user_id should be added to the session hash:
session[:user_id] = @user.id and the user should be redirected to
d) In the private
user_params method, allow all attributes except
:password_digest and replace that with
e) In the
new method, be sure to set @user = User.new
To do this, DO NOT run the rails generator as you will overwrite the view files I've given you. Just create an empty file called
users_controller.rb and build this controller manually. (Not hard; look at past projects/labs if you are unsure how to do this.)
We also need a SessionsController to handle logging in for users who already exist in the system. Create this file from scratch as well.
We need a new method which is essentially blank, but let's the user get a login form (provided). We need a create method which tries to authenticate and if successful sets the user_id in session. Finally, we need a destroy method for logout which destroys the user_id in session (clearing the session). In the interest of time, the code for all this can be seen below:
class SessionsController < ApplicationController def new end def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to home_path, notice: "Logged in!" else flash.now.alert = "Email or password is invalid" render "new" end end def destroy session[:user_id] = nil redirect_to home_path, notice: "Logged out!" end end
Note: if you created the class method earlier in the User model, you could use that instead to rewrite/replace the first two lines of the create action. This is optional, but it would be a good learning exercise at some point to do this and make sure you have a good grasp of what is happening when creating a user's session.
config/routes.rband add the following routes:
resources :users resources :sessions get 'user/edit' => 'users#edit', :as => :edit_current_user get 'signup' => 'users#new', :as => :signup get 'login' => 'sessions#new', :as => :login get 'logout' => 'sessions#destroy', :as => :logout # Default route root :to => 'bands#index', :as => :home
rails routes from the terminal to update the routes.
rails generate migration [NAME](remove the change method in this new migration) and add these methods:
def up adminGenre = Genre.new adminGenre.name = "Admin Genre" adminGenre.save! adminBand = Band.new adminBand.name = "Admin Band" adminBand.description = "An initial band to create users" adminBand.genre_ids = [adminGenre.id] adminBand.save! admin = User.new admin.first_name = "Admin" admin.last_name = "Admin" admin.email = "firstname.lastname@example.org" admin.band_id = adminBand.id admin.password = "secret" admin.password_confirmation = "secret" admin.role = "admin" admin.save! end def down admin = User.find_by_email "email@example.com" User.delete admin band = Band.find_by_name "Admin Band" Band.delete band end
rails db:migrate to get this user into the system.
<% if logged_in? %> <%= link_to 'Logout', logout_path %> <br>[<%= current_user.proper_name %>:<%= current_user.role %>] <% else %> <%= link_to 'Login', login_path %> <% end %>
Show a TA that you have the authentication functionality set up and working as instructed and that the code is properly saved to git. Make sure the TA initials your sheet.
With authentication under our belts, let's tackle the issue of authorization. We will be using the CanCan gem to help with this; feel free to open the documentation for this gem and reference it if you have questions. Using CanCan we will first tell Rails what each user role can do in the system (stored in a file called 'ability') and then test in our controllers and/or views whether that user can? access selected functions on the app.
We will start by defining some abilities. The cancan gem is looking for a model file called 'ability.rb' located in
app/models. Let's create this file by running on the command line
rails generate cancan:ability.
Looking at this file in the models directory, you can see it is an example of a non-ActiveRecord model. (Most, but not all models inherit from ActiveRecord. Since abilities are defined in that file, there is no need for database access so we don't need the power of ActiveRecord.) The initialize method is there (with lots of helpful comments), but we need to add some basic abilities. We also see that the method takes a user as an argument, but what if someone is not logged in yet? Will it blow up in our face?
To prevent this, we add the line
user ||= User.new to the initialize method.
Now it is time to add the all-powerful admin user; admins can do everything and guests can only read for now. To make this happen, add the following code to the initialize method:
if user.role? :admin can :manage, :all else can :read, :all end
The user model needs a method called
role? that compares a user's role in the system with the role we are testing for. So this can all work properly, add the following code to the User model:
ROLES = [['Administrator', :admin],['Band Manager', :manager],['Band Member', :member]] def role?(authorized_role) return false if role.nil? role.to_sym == authorized_role end
Now an admin can 'manage' (run all CRUD operations) for all models while guests can only read content (but again for all models).
Now that we have this simple authorization in place, time to go put constraints on the controllers so they don't give the user access to app functionality they aren't entitled to.
Open the band controller and add to the top of the
new action the following line:
authorize! :new, @band. What this is doing is raising an exception if the user does not have the ability to create a new band.
We will add the same line to the create method just in case someone is trying to add a new band without going through the interface (we will learn about this soon enough).
In the edit and update methods add the line
authorize! :update, @band to the update method and
authorize! :destroy, @band to the destroy method. You will need to comment out check_login
before_action callback to make sure that CanCan can do its job (otherwise, you will hit the
before_action and get redirected before the
authorize! command is called).
Test out your work by logging in as an admin (small login link in upper right corner) and see that you can access everything. Logout and try to access restricted functionality (go to bands/new); you should get a
CanCan::AccessDenied exception. If do not get this exception, please see a TA for assistance before going on further. Now we need to add similar restraints to the genre controller (try it and see that access is unrestricted), but you realize this could be tedious for a larger project to do this for every action in every controller. Not to worry, CanCan has a shortcut for us; add to the top of the genres_controller
authorize_resource and it will be as if you added the
authorize! method to each action in the controller. Note here, you will simply now just be redirected to the login action because
authorize_resource handles these exceptions nicely.
Let's fix up that exception page – informative to developers, but not appropriate for general users. Go to the Application controller and add the following code into
private (but not in a specific method) to fix up certain cases where the user is logged in but does not have appropriate authorization:
rescue_from CanCan::AccessDenied do |exception| flash[:error] = "Go away or I shall taunt you a second time." redirect_to home_path end
If you don't like Monty Python or you just want to follow some of the design principles discussed in class, you can change the text to a more appropriate message. [Quick: what is wrong with this message? If you don't immediately know the answer, go back and review your design notes and book.]
<% if can? :update, band %> <%= link_to ... %> <% end %>
What we are doing here is testing whether the user has the access rights of the user to see if he/she has the ability to update this particular band. Add a similar control to the delete icon and link. After that, fix the new band link with the following at the end:
<% if can? :create, Band %> <p><%= link_to "Create New Band", new_band_path %></p> <% end %>
elsethat sets the guest users, add the following:
elsif user.role? :manager can :update, Band do |band| band.id == user.band_id end can :destroy, Band do |band| band.id == user.band_id end elsif user.role? :member can :update, Band do |band| band.id == user.band_id end
What this says in each case is that the user (manager or member) has the ability to perform the operation specified on Band objects if the id of the band equals the user's band_id. Now if you log in as a manager [you'll need to first create genres, then a band, and then manager and members], you should see links for editing/deleting the band, but not for others. Likewise, logging in as a member should show the update functionality for the band is there and working, but others are not. Get the TA to verify this and mark off the checkpoint.
Show a TA that you have the authorization functionality set up for all three user types and working as instructed and that the code is properly saved to git. Make sure the TA initials your sheet.
If time allows during lab, challenge yourself by extending this project – add the ability of anyone (even guests) to write comments about a particular band. [This will require a Comment model and appropriate support from views and controllers.] Comments should be displayed only on the band's show details page and place a form for new comments should be there as well. Comments may be deleted only by an admin. (You shouldn't even see the option if you are not an admin.) Again this is optional but an excellent exercise when you have the time, but not essential for today.