My Rails student planner application has a few issues regarding URL tampering. I believe they probably all share a similar solution but I'm having difficulty.
When viewing an assignment (students/:id/assignments/:id), changing the assignment’s ID in the URL to the ID of an assignment belonging to another student sometimes leads to a "no method error" in my assignments#show page, other times it will show the other student's assignment, when ideally I'd like to just redirect back to their home page.
Similarly, this happens with the assignment's edit page (students/:id/assignments/:id/edit), course (students/:id/courses/:id) and course's edit page (students/:id/courses/:id/edit). Sometimes I'll get an "ArgumentError in Assignments#edit" when viewing an assignment's edit page.
I believe these should be able to be remedied in my controllers, so I've included my assignments_controller and courses_controller.
Assignments_controller:
class AssignmentsController < ApplicationController
before_action :require_logged_in
before_action :set_student
def new
if #student && #student.id == current_student.id
#assignment = Assignment.new
#courses = Course.where(student_id: current_student.id)
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def create
#assignment = Assignment.new(assignment_params)
#assignment.student_id = current_student.id if current_student
#courses = Course.where(student_id: current_student.id)
if #assignment.save
redirect_to student_assignments_path(#student)
else
render :new
end
end
def index
if #student && #student.id == current_student.id
#assignments = Assignment.where(student_id: current_student.id)
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def show
#student = Student.find_by(id: params[:student_id])
if #student && #student.id == current_student.id
##assignment = student.assignments.find_by(id: params[:id])
#assignment = Assignment.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def edit
if #student && #student.id == current_student.id
#assignment = Assignment.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def update
student = Student.find_by(id: params[:student_id])
#assignment = Assignment.find_by(id: params[:id])
#assignment.update(params.require(:assignment).permit(:title, :due_date))
redirect_to student_assignment_path(student, #assignment)
end
def destroy
#student = Student.find_by(id: params[:student_id])
#assignment = Assignment.find_by(id: params[:id]).destroy
redirect_to student_path(#student), notice: 'Assignment was successfully completed.'
end
private
def assignment_params
params.require(:assignment).permit(:title, :due_date, :course_id, :student_id)
end
def set_student
#student = Student.find_by(id: params[:student_id])
end
end
Courses_controller:
class CoursesController < ApplicationController
before_action :require_logged_in
before_action :set_student
def new
if #student && #student.id == current_student.id
#course = Course.new
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def create
if #student && #student.id == current_student.id
#course = Course.create(course_params)
#course.student_id = params[:student_id]
if #course.save
redirect_to student_courses_path(#student)
else
render :new
end
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def index
if #student && #student.id == current_student.id
#courses = Course.where(student_id: current_student.id)
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def show
#student = Student.find_by(id: params[:student_id])
if #student && #student.id == current_student.id
#course = #student.courses.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def edit
if #student && #student.id == current_student.id
#course = Course.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def update
student = Student.find_by(id: params[:student_id])
#course = Course.find_by(id: params[:id])
#course.update(params.require(:course).permit(:course_name))
redirect_to student_course_path(student, #course)
end
def destroy
#student = Student.find_by(id: params[:student_id])
#course = Course.find_by(id: params[:id]).destroy
redirect_to student_path(#student), notice: 'Course was successfully deleted.'
end
private
def course_params
params.require(:course).permit(:course_name)
end
def set_student
#student = Student.find_by(id: params[:student_id])
end
end
This line is the source of all the problems:
#assignment = Assignment.find_by(id: params[:id])
That's a huge mistake. I'd argue that you never, ever use the top-level model to fetch records that must be secured. The failure state of this code is a user sees everything. This is a problem that can't be fixed by patching over it with an access-control list. Those may not apply correctly every time, someone could find a loophole.
Instead you do this:
#assignment = #student.assignments.find_by(id: params[:id])
Worst-case scenario is you get a not-found error. It's impossible for someone to bypass this by hacking around with the URL. The failure state here is the record is not found.
If you want your URLs resistant to tampering you'll also want to use non-sequential identifiers. On MySQL it's often best to create a secondary column specifically for this purpose, like called param or slug or ident, whatever you prefer, and populate that with something random and harmless like:
before_validation :assign_slug
def assign_slug
self.slug ||= SecureRandom.uuid
end
Where that's indexed in your schema for quick retrieval. Where you have a student relationship:
add_index :assignments, [ :student_id, :slug ]
Postgres allows using UUID primary keys which might be verbose, but don't allow people to tinker and experiment to expose information. You really can't "guess" a randomized UUID value.
This might help you:
In your CoursesController and AssignmentsController, add a before_action in your controllers that will limit student's access.
#xxx_controller.rb
class XxxController < ApplicationController
before_action :require_logged_in
before_action :set_student
before_action :check_owner, only: [:show, :edit, :update, :destroy]
Then define the method in your ApplicationController:
#application_controller.rb
def check_owner
if #student.blank? || #student.id != current_student.id
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
I guess access restriction is what you are looking for. CanCanCan will help you to set proper access to pages.
Also please replace find_by(id: params[:id]) with find(id) because it is more readable and more efficient.
So you have authentication set up allowing access only to logged in users.
Now you need an authorization process to allow access to certain actions/resources depending on the user id or role.
One popular option is Pundit and the readme page should get you started.
Related
New to rails. Following a tutorial on polymorphic associations, I bump into this to set #client in create and destroy.
#client = Client.find(params[:client_id] || params[:id])
I'm normally only used to that you can only find #client = Client.find(params[:id])
so how does this work with there being two params? How does the || work?
FavoriteClientsController.rb:
class FavoriteClientsController < ApplicationController
def create
#client = Client.find(params[:client_id] || params[:id])
if Favorite.create(favorited: #client, user: current_user)
redirect_to #client, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #client, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#client = Client.find(params[:client_id] || params[:id])
Favorite.where(favorited_id: #client.id, user_id: current_user.id).first.destroy
redirect_to #client, notice: 'Leverandøren er nu fjernet fra favoritter'
end
end
Full code for controller, models can be seen here
Using rails 5
Expression: params[:client_id] || params[:id] is the same as:
if params[:client_id]
params[:client_id]
else
params[:id]
end
Wow thats an incredibly bad way to do it.
A very extendable and clean pattern for doing controllers for polymorphic children is to use inheritance:
class FavoritesController < ApplicationController
def create
#favorite = #parent.favorites.new(user: current_user)
if #favorite.save
redirect_to #parent, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #parent, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#favorite = #parent.favorites.find_by(user: current_user)
redirect_to #parent, notice: 'Leverandøren er nu fjernet fra favoritter'
end
private
def set_parent
parent_class.includes(:favorites).find(param_key)
end
def parent_class
# this will look up Parent if the controller is Parents::FavoritesController
self.class.name.deconstantize.singularize.constantify
end
def param_key
"#{ parent_class.naming.param_key }_id"
end
end
We then define child classes:
# app/controllers/clients/favorites_controller.rb
module Clients
class FavoritesController < ::FavoritesController; end
end
# just an example
# app/controllers/posts/favorites_controller.rb
module Posts
class FavoritesController < ::FavoritesController; end
end
You can then create the routes by using:
Rails.application.routes.draw do
# this is just a routing helper that proxies resources
def favoritable_resources(*names, **kwargs)
[*names].flatten.each do |name|
resources(name, kwargs) do
scope(module: name) do
resource :favorite, only: [:create, :destroy]
end
yield if block_given?
end
end
end
favoritable_resources :clients, :posts
end
The end result is a customizable pattern based on OOP instead of "clever" code.
The tutorial which teaches you to do
Client.find(params[:client_id] || params[:id])
is a super-duper bad tutorial :) I strongly recommend you to switch to another one.
Back to the topic: it is logical OR: if first expression is neither nil or false, return it, otherwise return second expression.
That thing is just trying to find client by client_id if there is one in the request params. If not it's trying to find client by id.
However such practic can make you much more pain than profit.
I have a set of nested routes that look like this:
resources :workouts do
resources :exercises do
resources :reports, shallow: true
end
end
I tried to make them as shallow as possible, but not nesting them simply generated too many errors.
Right now they're working fine except when I try to access the exercises#index page for a specific workout. I'm linking the page like this:
<%= link_to 'Add/Edit Exercises', workout_exercises_path(#workout, exercise), method: :index %>
And when I click that link I get:
ActiveRecord::RecordNotFound at /workouts/3/exercises
Couldn't find Workout without an ID
The error is called on my exercises_controller on the indicated line:
class ExercisesController < ApplicationController
before_action :authenticate_user!
def index
#workout = Workout.friendly.find(params[:id]) <<<<<THIS LINE
#exercise = Exercise.new
#exercises = Exercise.all
end
def new
#exercise = Exercise.new
end
def create
#workout = Workout.friendly.find(params[:id])
exercise = #workout.exercises.new(exercise_params)
exercise.user = current_user
if exercise.save
flash[:notice] = "Results saved successfully."
redirect_to [#workout]
else
flash[:alert] = "Results failed to save."
redirect_to [#workout]
end
end
def destroy
#workout = Workout.friendly.find(params[:workout_id])
exercise = #workout.exercises.find(params[:id])
if exercise.destroy
flash[:notice] = "Exercise was deleted successfully."
redirect_to [#workout]
else
flash[:alert] = "Exercise couldn't be deleted. Try again."
redirect_to [#workout]
end
end
private
def exercise_params
params.require(:exercise).permit(:name, :needs_seconds, :needs_weight, :needs_reps, :workout_id)
end
def authorize_user
exercise = Exercise.find(params[:id])
unless current_user == current_user.admin?
flash[:alert] = "You do not have permission to create or delete an exercise."
redirect_to [exercise.workout]
end
end
end
The friendly.find is because I am using the friendly_id gem, which generates slugs instead of ids in the url. I use it in the same manner in other working models, so I do not believe this is causing the current problem.
Can anyone see why I'm getting this error? I have typed #workout into the live shell of my error and it says that #workout is nil, which means it isn't getting the value somehow. I know this is important information, but don't know how to fix it.
The problem is:
Your route was /workouts/3/exercises, so it format like: /workouts/:workout_id/exercises
So just modify your create method a bit:
def create
#workout = Workout.friendly.find(params[:workout_id])
exercise = #workout.exercises.build(exercise_params)
...
end
def index
#workout = Workout.friendly.find(params[:workout_id])
#exercise = Exercise.new
#exercises = Exercise.all
end
Try this one, Hope this would help you.
I'm in the process of creating a website similar to Reddit. I would like to allow a moderator to be able to update a topic, but not be able to create or delete topic. I'm aware that I need to update TopicsController but I'm not sure how. My main problem is that I'm not sure how to make the code specific enough to ensure that a moderator can only update; not delete or create a topic, as an admin can.
My current code looks like this:
class PostsController < ApplicationController
before_action :require_sign_in, except: :show
before_action :authorize_user, except: [:show, :new, :create]
def show
#post = Post.find(params[:id])
end
def new
#topic = Topic.find(params[:topic_id])
#post = Post.new
end
def create
#post.body = params[:post][:body]
#topic = Topic.find(params[:topic_id])
#post = #topic.posts.build(post_params)
#post.user= current_user
if #post.save
flash[:notice] = "Post was saved"
redirect_to [#topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :new
end
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
#post.assign_attributes(post_params)
if #post.save
flash[:notice] = "Post was updated."
redirect_to [#post.topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :edit
end
end
def destroy
#post = Post.find(params[:id])
if #post.destroy
flash[:notice] = "\"#{#post.title}\" was deleted successfully."
redirect_to #post.topic
else
flash[:error] = "There was an error deleting the post."
render :show
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
def authorize_user
post = Post.find(params[:id])
unless current_user == post.user || current_user.admin?
flash[:error] = "You must be an admin to do that."
redirect_to [post.topic, post]
end
end
end
I've already added a moderator role to the enum role.
I apologise if this seems really basic...but it has got me stumped!
Thanks in advance!
I could answer with some custom solution, but it's better to use a more structured and community-reviewed approach: authorization with cancan.
As tompave noticed you can use cancan gem for this.
Personally I prefer pundit.
In old days I used to define permissions directly in code everywhere: in controllers, in views and even models. But it's really bad practice. When your app grows, you are lost: you update a view, but you should make the same change in controller and sometimes in model too. It soon becomes absolutely unmanageable and you have no idea what your users can and cannot do.
Pundit, on the other hand, offers central place -- policy -- for defining what user can do. Views and controllers can then use those policies.
For example, if you need to define Post's policy you simply create app/policies/post_policy.rb file:
class PostPolicy
attr_reader :user
attr_reader :post
def initialize(user, post)
#user = user
#post = post
end
def author?
post.user == user
end
def update?
author? || user.admin? || user.moderator?
end
def create?
author? || user.admin?
end
def destroy?
author? || user.admin?
end
# etc.
end
Now whenever you need to check user's ability to perform action, you can simply invoke:
# in controller
def update
#post = Post.find(params[:id])
authorize #post
# do whatever required
end
# in view
<% if policy(post).update? %>
<%= link_to 'Edit Post', post_edit_path(post) %>
<% end %>
As you can see Pundit is very easy to comprehend and it uses the same "convention over configuration" approach as Rails. At the same time it's very flexible and allows you to test virtually anything.
You will definitely need Pundit or any similar gem to manage permission in your ambitious app.
I keep getting a variety of error while trying to create and show errors in a simple Rails blog I'm trying to create.Let me know if you see anything obvious or if you need me to post more code as I've tried a number of things but to no avail. Thanks
The browser is giving me this error
Couldn't find User without an ID
in my "logged_in?" method which shows
def logged_in?
#current_user ||= User.find(session[:user_id])
end
Sessions Controller
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
flash[:success] = "You are logged in"
redirect_to root_path
else
render action: 'new'
flash[:error] = "There was a problem logging in. Please check your email and password"
end
end
end
def index
#users = User.all
end
def show
end
def new
#user = User.new
end
def edit
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
flash[:notice] = "You have registered, please login"
redirect_to login_path
else
render :new
end
end
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to #user, notice: 'User was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
def destroy
#user.destroy
respond_to do |format|
format.html { redirect_to users_url }
format.json { head :no_content }
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
end
Articles Controller
class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def new
#article = Article.new
end
def index
#article = Article.all
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
def edit
#article = Article.find(params[:id])
end
def update
#article = Article.find(params[:id])
if #article.update(article_params)
redirect_to #article
else
render 'edit'
end
end
def show
#article = Article.find(params[:id])
end
def destroy
#article = Article.find(params[:id])
#article.destroy
redirect_to articles_path
end
private
def article_params
params.require(:article).permit(:title, :text, :image)
end
end
Application Helper
module ApplicationHelper
def logged_in?
#current_user ||= User.find(session[:user_id])
end
end
The problem you're facing is that session[:user_id] is nil. Usually a method which sets current user is called current_user. The logged_in? is not a good name for a method setting an user instance, because one would expect that a method ending with a question mark would return a true or false. And not an user instance.
Also, setting the current user is usually done with a before_filter. Additionally, you want to skip such before filter for action where you're setting the current user (i.e the current_user doesn't exist yet)
Finally, I would rather fail gracefully, if user is not found. You can achieve this by changing your code to User.find_by_id(session[:user_id])
While the user is not loggued, session[:user_id] is nil, and so User.find(session[:user_id]) generates the error. The method should be like this:
def logged_in?
#current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
Why would the logged_in? helper method try to assign a value to #current_user? I think that is a bad logic, it should just return a boolean result without modifying such a central instance. This is a proper way to do that:
def logged_in?
#current_user.nil? ? false : true
end
The responsibility of setting the #current_user falls to a method that you should place in application_controller.rb and make it a before_action so that it's executed before any controller action is triggered, that is:
# app/controllers/applicaton_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_action :authenticate_user
# Your actions here
..
..
#
private
def authenticate_user
#current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
end
I am trying to block all default methods except create and update in my users controller using declerative_authorization. But at the time I add filter_resource_access or filter_access_to into my usersController i always get "Couldn't find User without an ID". Anyone care to explain why this could be happening?
class UsersController < ApplicationController
filter_resource_access
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
flash[:notice] = "Account registered!"
redirect_to account_url
else
render :action => :new
end
end
def show
#user = #current_user
end
def edit
#user = #current_user
end
def update
#user = #current_user # makes our views "cleaner" and more consistent
if #user.update_attributes(params[:user])
flash[:notice] = "Account updated!"
redirect_to account_url
else
render :action => :edit
end
end
end
You should set the #user variable before the filter_access_to call with a before_filter as declarative_authorization tries to access #user when you call filter_access_to.
before_filter :set_user
filter_access_to :all
...
protected
def set_user
#user = #current_user
end
Maybe you are setting the attribute_check parameter to true in your filter_access_to call? I have a similar controller and I don't really need the before_filter.
Another thing that might be causing it is a using_access_control call in your User model.