I have a profiles controller where user are allowed to access their own profile, but not others users profile.
When I accessing an url like : http://localhost:3000/en/profiles/2. I want my user to be redirected to their own profile if the URL correspond to disown profile. How I can handle this ?
My actual show action in my profiles controller looks like :
def show
#profile = Profile.find(params[:id])
#user = current_user
#profile = #user.profile
end
I have already try this method but doesn't work
def correct_user
redirect_to(profile_path) unless current_user == (#profile.user if #profile)
end
You should probably move the logic to a before_action.
before_action :find_profile, :enforce_current_profile
def show
end
protected
def find_profile
#profile = Profile.find(params[:id])
end
def enforce_current_profile
unless #profile && #profile.user == current_user
redirect_to(profile_path)
end
end
However, what you really want to do, is to convert the profile controller to a resource rather than a resources in your route file.
resource :profile
In this way, Rails will generate
GET /profile
rather than
GET /profile/2
and you will not need any control. Just set
def show
#user = current_user
#profile = #user.profile
end
Using a resource instead of a resources will not provide the index action, but I doubt you need it.
Related
I'm building an events app with users who will each have a personal profile. I've set up a few users for the site but when I try and create and/or edit a profile for each user it refers me back to a flash message "That profile doesn't belong to you!" which is in reference to my first user profile which was set up and works fine.
I'm using Devise gem for initial set up but have built out from their with my own user controller. Here's the code from that controller -
class UsersController < ApplicationController
before_action :authenticate_user!
before_action :set_user
before_action :owned_profile, only: [:edit, :update]
def new
#user = User.new
end
def show
#user = User.find(params[:id])
end
def create
end
def edit
#user = current_user #User.find_by(params[:id])
end
def update
#user = User.find_by(params[:id])
if #user.update(user_params)
redirect_to user_path, notice: "Profile successfully updated!"
else
render 'edit'
end
end
private
def user_params
params.require(:user).
permit(:name, :username, :biography, :email, :url)
end
def owned_profile
unless current_user == #user
flash[:alert] = "That profile doesn't belong to you!"
redirect_to root_path
end
end
def set_user
#user = User.find_by(params[:id])
end
end
Any assistance would be appreciated.
I would create an admin. An easy way to do this is to add a column to your users table called admin and make it a boolean. Migrate the db.
Then check to whether a user is an admin before running the owned_profile method. In that method, change: unless current_user == #user to
unless current_user == #user || current_user.admin
Then set yourself as an admin in the console, save and then freely add profiles without that callback running.
If the issue is that Users are not able to edit their own profile, then I believe it is caused by the use of find_by within set_user:
#user = User.find_by(params[:id])
Should be:
#user = User.find(params[:id])
If you truly wanted to use find_by you could do:
#user = User.find_by_id(params[:id])
Or
#user = User.find_by(id: params[:id])
Find_by used as the 2 examples above will not throw an error if a User is not found, while find will.
Sidenote: You can remove the #user assignment within the show action.
You can do it by this way.
When user signing up, automatically creates profile. Good point of this ID of user and profile tables will be the same.
rails g model profile first_name last_name email
rails g migration add_user_id_to_profiles user_id:integer
Profile.rb
belongs_to :user
User.rb
has_one :profile, dependent: :destroy
before_create :set_profile
def set_profile
build_profile(id: self.id, user_id: self.id, email: self.email)
end
GoodLuck.
I'm trying to make sure that people can't submit a create action if they submit an entry with an ID other than their own. For this, I have set up the test as following:
entries_controller_test.rb
def setup
#user = users(:thierry)
#other_user = users(:steve)
end
...
test "should redirect create action on entry with id that doesn't belong to you" do
log_in_as(#user)
assert_no_difference 'Entry.count' do
post :create, entry: { content: "Lorem Ipsum"*10, id: #other_user }
end
end
The outcome of the test is that Entry.count increases by one, therefore #user can create a post with ID #other_user (is the code correct to create an entry with ID of the other user?)
entries_controller.rb: My create action currently looks like this.
def create
#entry = #entries.build(entry_params)
if #entry.save
flash[:success] = "Your entry has been saved."
redirect_to root_path
else
flash.now[:danger] = "Your entry has not been saved."
render 'index'
end
end
The instance variable is being passed in to the action by calling before_action :correct_user on the action. Here's the correct_user method.
def correct_user
#entries = current_user.entries
redirect_to root_url if #entries.nil?
end
By the way, the create action is being called from the index page. I suspect the problem is indeed with authorization since my test can log in the user and create an actual entry.
Can anyone spot an issue?
Your code is only checking whether the current_user has some entries, but there is no validation on the user_id of the entry being submitted to the create action. Moreover, even if the user has no entries, the #entries variable will be [], which is not nil (so correct_user will never redirect to root). The correct check would have been #entries.empty?, but still the object would be created with an incorrect user, as long as the current_user already has some entries belonging to them.
The way I usually go about this is not to permit the user_id parameter (with strong_parameters), and by setting the ownership of new objects to the current_user. If you want to perform the check, your correct_user should look more like this:
def correct_user
unless current_user.id == params[:entry][:user_id]
flash[:alert] = "Some error message"
sign_out # This action looks like a hack attempt, thus it's better to destroy the session logging the user out
redirect_to root_url
end
end
I think this might work.
In your entries controller.
class EntriesController < ApplicationController
before_action :correct_user, only: [:edit, :update]
def correct_user
unless correct_user.id == params[:entry][:user_id]
else
redirect_to root_url
end
end
end
The rails before action seems useful for setting a variable shared by a number of actions in a controller.
But isn't the default implementation of the set_post that we see commonly on tutorials etc open to an attack by a malicious user?
If we take a controller like this:
PostsController < Application Controller
before_action :set_post , only: [:show,:create,:update]
def show
...
end
def create
...
end
def update
...
end
private
def set_post
#post = Post.find(params[:id])
end
end
When a user is presented the opportunity to update a post for example the form would be generated for them, and on post, params[:id] would contain the ID of the appropiate post - probably owned by the current_user.
However, it would not be difficult for a malicious user to alter the posted :id variable to allow them to actually end up setting the #post variable in the controller, to represent a different post, rather than the original being updated.
I could see this being safer:
private
def set_post
#post = Post.find(params[:id])
if(#post.user_id != current_user.id)
redirect_to homepage, alert: "you can edit your own posts"
end
end
However - that would stop other users viewing other people's posts! So how and where should this kind of check be performed to ensure that only the owner of a particular post can update / edit it. Is that something for the update controller action to handle itself with a check like this :
def update
if #post.user_id != current_user.id
redirect_to homepage, alert: "you can edit your own posts"
end
...
end
You are right, and I actually see that security issue being made very often by newbie Rails programmers. They just generate scaffolds and don't change things to their needs.
I'm using something like the following in my controllers:
before_action :set_post
before_action :check_post_ownership, except: :show
private
def set_post
#post = Post.find(params[:id])
end
def check_post_ownership
redirect_to homepage, alert: "..." unless #post.user_id == current_user.id
end
i have a rails app with sorcery
everything work .
the problem is when edit a user like :
http://localhost:3000/users/1/edit
its work fine , but when i change the user id to 2 or 3 ..
i can update all users data
how can i restrict the edit page only if the current user is the one that logged in
here is my controller :
skip_before_action :require_login, only: [:new, :create, :show]
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
auto_login(#user)
flash[:info] = "Welcome."
redirect_to root_url
else
render 'new'
end
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to #user
else
render 'edit'
end
end
def show
#user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
you can also do something like this
before_action :edit_rights?, only: [:update, :edit]
private
def edit_rights?
#user = User.find(params[:id])
redirect_to(root_path) unless current_user == #user
end
you won't need #user = User.find(params[:id]) in both update and edit actions then
There are (at least) two ways to do that. First and straightforward is detailed in another answer, fine-tune your controller.
A less obvious way is to create a singular resource and its own controller. In routes that could look like:
resource :profile, only: [:show, :edit, :update]
# generates:
# /profile (GET, PATCH, PUT)
# /profile/edit (GET)
Then create a controller that is responible solely for user's own profile and operates only on current_user.
Yes, it's okay for one model to have multiple controllers, if your model should behave really differently in different parts of your app.
Why would you do that?
User's own profile could show much more information than is available publicly, you can lay it out in a separate view
No "access denied" errors, as the resource is auto-selected via current_user, all you need is ensure the user is logged in in the entire controller.
So I am using omniauth-facebook to create a log in using Facebook.
Here is my sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to "/sessions/menu"
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
def new
end
def menu
puts user.name
end
end
Unfortunately I don't know how to access the user variable in the menu action. How would you guys recommend I do this?
Update
class SessionsController < ApplicationController
def create
#user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = #user.id
redirect_to "/sessions/menu"
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
def new
end
def menu
puts #user
end
end
Even when I update it like so, it doesn't work
Unfortunately I don't know how to access the user variable in the menu
action. How would you guys recommend I do this?
Every time a request is made in your app for actions in SessionsController, a new instance of the SessionsController is created. So, instance variable set during create action would not be available when request for menu action is called as now you have a new instance of SessionsController which is why #user set in create action would not be available in menu. Also, if you use user (local variable) in this case, then its always local to the method/action in which its defined. So even that would not be available in menu.
By using facebook-omniauth gem you would receive Auth Hash in env["omniauth.auth"] which in turn you would use to create(new user) or initialize(in case of existing user) a user hopefully in from_omniauth method.
NOTE: env is request specific so env["omniauth.auth"] value will be present in create action but not in menu.
To resolve this, i.e., to access the created or initialized facebook user in menu action, you should make use of the user_id that you stored in session as below:
def menu
if session[:user_id]
#user = User.find(session[:user_id])
end
end
Also, if you would like to access the user in other actions as well then I would recommend to reuse the code by using before_action callback:
class SessionsController < ApplicationController
## Pass specific actions to "only" option
before_action :set_user, only: [:menu, :action1, :action2]
#...
def menu
puts #user.name
end
private
def set_user
if session[:user_id]
#user = User.find(session[:user_id])
end
end
end
where you can add specific actions via :only option