Resource Ownership Restricting Write-Access to Owned Resources
In this demonstration, I will show how to implement user-ownership of model objects in an app so that only the owner will be able to modify or delete them.
General Steps
In general, the steps for implementing user-ownership are as follows.
-
Step 1 Create Ownership Association. This step creates a one-to-many association between User and the owned model class, and also updates the seeds and the views.
-
Step 2 Restrict Access to Owned Class. This step restricts write-access to the owned model class for anyone who is not the owner.
-
Step 3 Restrict Access to Child Classes of Owned Class (If Needed) This step restricts write-access to any child classes of the owned model class for anyone who is not the owner.
Implementing User-Ownership of Quizzes
To demonstrate the steps for implementing user-ownership, we will be adding it to the Quiz Me app so that users have control of the quizzes they create and only those that they create.
Step 1 Create Ownership Association
We will implement a one-to-many relationship between User and Quiz such that a User has many Quizzes. If you need a review on implementing a one-to-many association, please reference the demo Adding a One-to-Many Association.
Substep Create Foreign Key Migration. To create the foreign key migration, we run the command to generate the migration:
rails g migration AddUserFkColtoQuizzes
Then we add the reference to the migration like so:
class AddUserFkColtoQuizzes < ActiveRecord::Migration[6.0]
def change
add_reference :quizzes, :user, foreign_key: true
end
end
Substep Link Models. To link the User and Quiz models, we add the following to the User and Quiz model files.
In app/models/user.rb
:
has_many(
:quizzes,
class_name: 'Quiz',
foreign_key: 'user_id',
inverse_of: :creator
)
In app/models/quiz.rb
:
belongs_to(
:creator,
class_name: 'User',
foreign_key: 'user_id',
inverse_of: :quizzes
)
Then we can update the Quiz index and show pages to use our association.
In app/views/quizzes/index.html.erb
, add a column for the quiz creator that displays the associated user’s email using @quiz.creator.email
:
<table class="table table-borderless table-hover">
<thead>
<tr>
<th> Title </th>
<th> Description </th>
<th> Creator </th>
<th></th>
</tr>
</thead>
<tbody>
<% @quizzes.each do |quiz| %>
<tr>
<td> <%= quiz.title %> </td>
<td> <%= quiz.description %> </td>
<td> <%= quiz.creator.email %> </td>
<td class="text-nowrap">
<%= link_to 'show', quiz_path(quiz), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'edit', edit_quiz_path(quiz), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'delete', quiz_path(quiz), method: :delete, class: 'btn btn-outline-secondary btn-sm' %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'New Quiz', new_quiz_path, class: "btn btn-secondary" %>
</p>
Also, add the creator to the quiz show page like so:
<h1><%= @quiz.title %></h1>
<p>Created by: <%= @quiz.creator.email %></p>
<p>
<%= @quiz.description %>
</p>
<p>
<%= link_to 'Questions', quiz_questions_path(@quiz), class: 'btn btn-secondary' %>
</p>
<p>
<%= link_to 'Home', root_path, class: 'btn btn-outline-secondary' %>
</p>
Substep
Edit Quiz Create Action to Use the Association. This is similar to something we did in the previous association demo where instead of using @quiz.new
, we have to build it using its parent model object. We want to associate a new quiz with the user that is creating it, so we will use Devise’s current_user
helper instead of using the .find()
method with a user id.
def create
@quiz = current_user.quizzes.new(params.require(:quiz).permit(:title, :description))
if @quiz.save
flash[:success] = "New quiz successfully created!"
redirect_to quizzes_url
else
flash.now[:error] = "New quiz creation failed"
render :new
end
end
Substep
Update Fixtures and Seeds. To update the fixtures and seeds, we add a creator
attribute to each quiz and assign a user to it.
user1 = User.create!(
email: "bob@email.com",
password: "password"
)
user2 = User.create!(
email: "alice@email.com",
password: "password"
)
q1 = Quiz.create!(
title: 'American History 1',
description: 'American History 1776 - 1800',
creator: user1
)
# Quiz 1 Questions...
q2 = Quiz.create!(
title: 'Chemistry - Element Names',
description: 'Names of all of the elements of the periodic table',
creator: user1
)
quiz3 = Quiz.create!(
title: 'Math - Algebra 2',
description: 'Completing the Square to solve equations',
creator: user2
)
Test It!
To confirm that we made this change correctly, we run rails db:migrate:reset db:seed
, navigate to localhost:3000/quizzes
and see that creators are listed on the table.
Step 2 Restrict Access to Owned Class
To keep most people from editing or deleting quizzes that aren’t theirs we can use an if-statement to keep the edit and delete links from displaying on the index page if a user is not the given quiz’s creator.
This won’t stop everyone, however. If a user wanted to manually type in the URL to edit someone else’s quiz or delete it, they could do so. To stop them, we will add a before action similar to before_action :authenticate_user!
which will redirect anyone who is not the creator of the quiz to the index if they try to edit or delete that quiz.
Substep Hide links. To hide the edit and delete links, put an if-statement around those buttons on the index page checking the current user.
<table class="table table-borderless table-hover">
<thead>
<tr>
<th> Title </th>
<th> Description </th>
<th> Creator </th>
<th></th>
</tr>
</thead>
<tbody>
<% @quizzes.each do |quiz| %>
<tr>
<td> <%= quiz.title %> </td>
<td> <%= quiz.description %> </td>
<td> <%= quiz.creator.email %> </td>
<td class="text-nowrap">
<%= link_to 'show', quiz_path(quiz), class: 'btn btn-outline-secondary btn-sm' %>
<% if quiz.creator == current_user %>
<%= link_to 'edit', edit_quiz_path(quiz), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'delete', quiz_path(quiz), method: :delete, class: 'btn btn-outline-secondary btn-sm' %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= link_to 'New Quiz', new_quiz_path, class: "btn btn-secondary" %>
</p>
Substep
Add before_action. At the top of the Quizzes Controller, add a before_action
statement which will run a custom function require_permission
before the actions edit
, update
, and destroy
. We’ll also add the custom function beneath all of the controller actions.
class QuizzesController < ApplicationController
before_action :authenticate_user!, except: [:index]
before_action :require_permission, only: [:edit, :update, :destroy]
# Controller Actions...
def require_permission
end
end
Inside the function, we need to check the creator of the quiz against the currently logged in user. If they don’t match, redirect to the quizzes index with an appropriate message.
class QuizzesController < ApplicationController
before_action :authenticate_user!
before_action :require_permission, only: [:edit, :update, :destroy]
# Controller Actions...
def require_permission
if Quiz.find(params[:id]).creator != current_user
redirect_to quizzes_path, flash: { error: "You do not have permission to do that." }
end
end
end
The reason this works is that the before_action
filter can halt the request cycle, stopping the original action from ever executing.
Test It!
To confirm that we made this change correctly, we should sign in to the app as a new user and navigate directly to http://localhost:3000/quizzes/2/edit. You should be redirected to the index page with the message “You do not have permission to do that.” because we have set the creator of quiz2
to be Bob@email.com
.
If you then sign out and re-sign in as Bob, you can then navigate to http://localhost:3000/quizzes/2/edit and you will be granted access to edit the quiz.
Step 3 Restrict Access to Any Child Classes of Owned Class
Unfortunately just restricting access to the Quiz edit/delete links and controller actions won’t stop an unauthorized user from creating or editing the questions of a Quiz that isn’t theirs. We can solve this by doing the same kind of thing with the Question links and controller actions accessed through their parent.
Substep Hide links. To hide the new, edit, and delete links, put an if-statement around those buttons on the index page checking the current user of the parent Quiz.
<h1>Questions</h1>
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Question</th>
<th></th>
</tr>
</thead>
<tbody>
<% @questions.each do |question| %>
<tr>
<td><%= question.question %></td>
<td class="text-nowrap">
<%= link_to 'show', quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
<% if @quiz.creator == current_user %>
<%= link_to 'edit', edit_quiz_question_path(@quiz, question), class: 'btn btn-outline-secondary btn-sm' %>
<%= link_to 'destroy', quiz_question_path(@quiz, question), method: :delete, class: 'btn btn-outline-secondary btn-sm' %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% if @quiz.creator == current_user %>
<p>
<%= link_to "New Question", new_quiz_question_path(@quiz), class: "btn btn-secondary"%>
</p>
<% end %>
<p>
<%= link_to 'Quiz', quiz_path(@quiz), class: 'btn btn-outline-secondary' %>
</p>
Substep
Add before_action. In the Questions Controller, we need to add the same kind of before_action
and require_permission
function as we did for Quiz with 2 key differences. The first difference is that we need to restrict the new and create controller actions as well as edit, update, and destroy to keep the action logic simple since we can’t build directly off current_user
. The second difference is that, in the require_permission
function, we will use the Quiz’s id from the :quiz_id
parameter to check the creator since that is part of every action’s route.
class QuestionsController < ApplicationController
before_action :authenticate_user!
before_action :require_permission, only: [:new, :create, :edit, :update, :destroy]
# Controller Actions...
def require_permission
@quiz = Quiz.find(params[:quiz_id])
if @quiz.creator != current_user
redirect_to quiz_path(@quiz), flash: { error: "You do not have permission to do that." }
end
end
end
Test It!
To confirm that we made this change correctly, we should sign in to the app as a new user and navigate directly to http://localhost:3000/quizzes/1/questions/1/edit. You should be redirected to that quiz’s show page (http://localhost:3000/quizzes/1) with the message “You do not have permission to do that.” because we have set the creator of quiz2
to be bob@email.com
.
If you then sign out and re-sign in as Bob, you can then navigate to http://localhost:3000/quizzes/2 and you will see the new, edit, and delete links. You should also be allowed to use them to add or edit the questions.
Conclusion
Following the above steps, we have now restricted the ability of users to edit and delete quizzes that do not belong to them.