Welcome, Guest User :: Click here to login

Logo 67272q

Lab 7: BandBlitz

Due Date: April 12

Objectives

  • 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

README.md

Understanding Rails Authentication and Authorization using BandBlitz

This lab will serve as an introduction to working with sessions, authentication, and authorization using the Ruby on Rails framework.

Part 1: Sessions and Authentication

  1. We are going to be working with a project known as BandBlitz. This app allows for bands to post information about themselves as well as a small musical sample. It also allows guests to post comments about the band for others to see. Unregistered users can read everything, but can only post comments. If a band manager is made a user, he/she can update the band's information and remove the band from BandBlitz if they so desire. Regular band members can update the information, but cannot delete the band's entry. Administrators can do it all – all CRUD operations on both bands and genres and is the only user that can delete a comment left for a band (in case there is libel, obscene remarks, etc.). Begin by getting the base project code off of GitHub with the following command:

        git clone https://github.com/ladyhodhod/Lab7-BandBlitz-Starter.git
    

Once you get the code, run bundle install to get the gems such as CanCanCan we will need for this lab.

  1. We want to add authorization, but we must first begin by adding authentication. To do this, create a user model with the following attributes:
    first_name (string), last_name (string), email (string), role (string), password_digest (string), band_id (integer), active (boolean)

    Use rails generate model; 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.

  2. In the User model, create a relationship to Band (and likewise from Band to User). Note a band has_many users and a user belongs_to a band.

  3. 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 first_name, last_name, and email must be present, email is unique and email follows an email regex pattern (such as the one in PATS).

    validates_format_of :email, with: /\A[\w]([^@\s,;]+)@(([\w-]+\.)+(com|edu|org|net|gov|mil|biz|info))\z/i, message: "is not a valid format"

Add the following class method to the User model. This is to handle the 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?

  1. We are going to go to the ApplicationController (controllers/application_controller.rb) and add some methods we want all controllers to have for authentication purposes. The first will be the 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 create 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_login that 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
  1. 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.

  2. 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)
b) edit and update should get the 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 home_path
d) In the private user_params method, allow all attributes except :password_digest and replace that with :password and :password_confirmation
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.)

  1. 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 that is essentially blank but lets the user get a login form (provided). We need a create method that 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.authenticate(params[:email], params[:password])
      if user 
        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
  1. Now we have controllers and the views were already given to us, but without routes, these controllers will never be called. So go to config/routes.rb and 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

Now run rails routes from the terminal to update the routes.

  1. Now we will add a default user (admin) to the system using migrations (since all new sign-ups are going to be members-only unless an admin is signing them up and chooses a different level). An example of the up and down methods for this migration are below; create a new migration with rails generate migration AddDefaultUser (remove the change method in this new migration) and add these methods:
  def up
    adminBand = Band.new
    adminBand.name = "Admin Band"
    adminBand.description = "An initial band to create users"
    adminBand.save
    admin = User.new
    admin.first_name = "Admin"
    admin.last_name = "Admin"
    admin.email = "admin@example.com"
    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 "admin@example.com"
    User.delete admin
    band = Band.find_by_name "Admin Band"
    Band.delete band
  end

Now run rails db:migrate to get this user into the system.

  1. Now we will test this out by attempting to log in as the default user. Start the server and try to log-in with the email and password we have in the migration (navigate to /login). This seems to work (you get a flash message saying 'Logged in!') but it would be nice to add some personal information to the page. In the application layout file, add to the div id="login" the following and reload the page to verify:
  <% if logged_in? %>
    <%= link_to 'Logout', logout_path %>
    <br>[<%= current_user.proper_name %>:<%= current_user.role %>]
  <% else %>
    <%= link_to 'Login', login_path %>
  <% end %>

Part 2: Authorization

  1. 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.

  2. 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.

  3. 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).

  1. 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).

  2. Test out your work by logging in as an admin (small login link in the 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. 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.

  3. 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
``

7. Time to clean up the views a bit. Go to the index page of the bands view and you will notice that there are three sets of comments telling us to essentially replace with some type of access control.  For the first one  edit icon and link  wrap the line in the following:

```erb
  <% 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 %>
  1. This is nice, but what about managers and members? They have partial access to update|destroy, but only for their own band. To do this, we have to modify 'ability.rb' to include these users. In the file after the admin is defined, but before the else that 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.

Bonus:5 points: If you turn up your submission 48 hours before the deadline.

Submission:
Your lab will be turned in via canvas as a zipped folder and git hub private remote URL -- at 11:59 pm on April 12, 2020. The private GitHub URL should be shared with Prof. Houda [username: ladyhodhod] and Preetha [username: preethagopinath]. We will access your Lab7 solution from this GitHub repository.

Instructions on adding us as a collaborator in Github: https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository