How can I block url adress in Devise (Ruby on Rails) - ruby-on-rails

I blocked display links on the show page:
<% if #post.user == current_user %>
links
<%end%>
but I can't block url adress for unprivileged users:
http://localhost:3000/posts/1/edit
What can I do?

It's good possibility to use Pundit gem (https://github.com/elabs/pundit).
Your policy will look:
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
#user = user
#post = post
end
def edit?
post.user == user
end
end
And your controller's action:
def edit
#post = Post.find_by(id: params[:id])
authorize #post
...
end

What you're looking for is something called authorization
Authentication = finding out if a user is present
Authorization =
determining if they are able to perform specific requests
The answer by Sergei Stralenia is correct - you'll need to use one of the authorization gems -- Pundit and CanCanCan being two of the most popular -- to validate whether a user is able to edit a particular object.
In regard the routing, you'll not be able to remove the edit route, unless you separate it out into something like an admin namespace (I'll explain more in a second).
--
Sergei Stralenia's post showed you how to use Pundit, I'll show you CanCanCan:
#app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, Post
else
can :read, Post
end
end
end
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def edit
#article = Post.find params[:id]
authorize! :edit, #article
end
end
Admin
If you wanted to make a post only editable in an "admin" area, you'd be best using something like the following:
#config/routes.rb
resources :posts, only: [:index, :show]
namespace :admin do
resources :posts, only: [:new, :create, :edit, :update, :destroy]
end
This way, you will literally have no way for a non-admin user to edit/update posts in the front-end. Instead, they'll have to go into the admin area and make it so that they are able to edit it in there...
#app/controllers/admin/posts_controller.rb
class Admin::PostsController < ApplicationController
#actions & authorization in here
end

Within the edit action on your controller, perform the same check - something like:
#post = Post.find_by( id: params[:id] )
unless #post.user == current_user
fail(ActionController::RoutingError, 'User cannot edit this post.')
end
You can simplify the error check into:
fail(ActionController::RoutingError, 'User cannot edit this post.') unless #post.user == current_user
I hope this helps!

I guess the best way to do this is to use before_filter in your posts controller, i.e.:
before_action :authorize_admin, only: [:show, :edit, :update, :destroy]
or:
before_filter :authorize_admin, except: [:show]
where :authorize_admin is the method that You have to define either in posts controller (to use for posts only) or in application controller (to use in all controllers), like this:
def authorize_admin
redirect_to :new_user_session unless current_user&&current_user.admin?
end

Related

Namespace an admin area

As an example, lets say I'm creating a twitter(-ish) clone.
A User has_many Tweets
A Tweet belongs to a User
Users can create tweets
Users can edit and delete their own tweets
Users who are admins can edit and delete all tweets
Users who are admins can edit and delete users who aren't admins
Here's my tweets controller:
class TweetsController < ActionController
before_action :set_tweet, only: [:edit, :update, :destroy]
before_action :only_admins_and_owner, only: [:edit, :update, :destroy]
def edit
# edits the tweet
end
def update
# updates the tweet
end
def destroy
# destroys the tweet
end
private
### security
def only_admins_and_owner
redirect_to root_url unless current_user.is_admin? || current_user === #tweet.user
end
###
def set_tweet
#tweet = Tweet.find(params[:id])
end
end
My users controller:
class UsersController < ActionController
before_action :set_user, only: [:edit, :update, :destroy]
before_action :only_admins_and_user, only: [:edit, :update, :destroy]
def edit
# edits the user
end
def update
# updates the user
end
def destroy
# destroys the user
end
private
### security
def only_admins_and_user
redirect_to root_url unless current_user.is_admin? || current_user === #user
end
###
def set_user
#user = User.find(params[:id])
end
end
And here's my panels controller. Not sure if this is the right way to do it to be honest. What do you think of my naming conventions? I haven't called it AdminController because by panels (plural) I am refering to the multiple panels in the admin area, the user panel (for displaying all of the users and offering administration controls in the view) and the tweets panel:
class PanelsController < ActionController
before_action :only_admins
def users
#users = User.all
end
def tweets
#tweets = Tweet.all
end
private
### security
def only_admins
redirect_to root_url unless current_user.is_admin?
end
###
end
If you think this controller setup is okay, how should I configure my routes to use these actions?
My routes file:
MyApp::Application.routes.draw do
resources :users, except: [:index] do
resources :tweets, except: [:index]
end
end
Now the above works okay if I am a normal user CRUDing away at user and tweets but how should I namespace my Panels controller?
When an admin is viewing users in the panels controller's users view, I want the URLs to look like this:
/control_panel/users
/control_panel/tweets
and when editing in the admin area:
/control_panel/users/12/edit
/control_panel/users/12/tweets/142/edit
but when a user is editing their own user or tweet:
/users/12/edit
/users/12/tweets/142/edit
This is because the panels administration views are vastly different to the user views, but the functionality of editing, updating and deleting is identical so I want to use the already existing actions. Am I doing it right? Not sure how else I could do it, other than adding loads of actions to to the panels controller, def_user_update and def_tweet_update and so on for every single resource. Doesn't feel very nice...
So how should I configure my routes?
I guess I want to sort of create an optional namespace around my two nested routes...
Maybe a concern? If I do that, though, I get an uninitialized constant Panel error.
If you want to use the same controller, which is something I don't really recommend, you can use a scope.
resources :users, except: [:index] do
resources :tweets, except: [:index]
end
scope :control_panel do
resources :users, expect: [:index] do
resources :tweets, except: [:index]
end
end
This gets you all the routes that you want, and they all point to the UsersController and TweetsController
What I recommend instead is using a different controller for admins. You can achieve this with namespaces.
namespace :control_panel do
resources :users, etc: ...
end
You then keep site user's concerns in TweetsController and control panel concerns in ControlPanel::TweetsController in app/controllers/control_panel/tweets_controller.rb
You can read more about namespaces and routing here.

Ruby on Rails: The same check in multiple actions in a controller

Frstly my apologies if this is a duplicate question. I have tried to find the answer but as Im very new to Rails I did not know what to search for.
I have a controller that has some security on it. For the show, edit, update and destroy actions I need to check if the user owns the persona they are working on, like this:
if #persona.user_id != #current_user.id
flash[:notice] = "Sorry, we couldn't find that persona"
redirect_to '/personas/'
else
# do something else
This is relatively easy. However, how do I do this in a DRY way? The code before the else is repeated across all 4 actions, the code after the else statement will be different on a per controller basis.
Thanks in advance.
Richard
You will need to use a before_filter. Something like this:
class PersonasController < ApplicationController
before_filter :check_owner, :only => [:show, :edit, :update, :destroy]
def show
#...
end
#...etc.
protected
def check_owner
redirect_to personas_path unless params[:id] == current_user.id
end
end
Also, take #davidb's advice on writing a current_user method if you don't already have one, which will go in your application_controller.rb. Something like this:
class ApplicationController < ActionController::Base
helper_method :current_user
def current_user
#current_user ||= session[:user_id] ? User.find(session[:user_id]) : User.new
end
end
You may have to tweak all of this, since it will depend on how you have your models setup. This is just a general idea of what you need to/should do.
Use before_filter here is an overview:
http://guides.rubyonrails.org/action_controller_overview.html#filters
To dry it you should also write a current_user method that returns the logged in user!
You can move your security logic to the before_filter. It will run before your actions and make your security check.
Your controller file:
class TestController
before_filter :check_persona, :only => [:show, :edit, :update, :destroy]
private
def check_persona
if #persona.user_id != #current_user.id
flash[:notice] = "Sorry, we couldn't find that persona"
redirect_to '/personas/'
end
end
end

Understanding Cancan abilities

Trying to get Cancan securing a few models in an application and curious why it's not working the way I thought it would. I had thought you could can? on the specific instance as opposed to the entire class so, not in this example but, you could enable abilities on a per instance basis as a list of posts are displayed?!?
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.role? :admin
can :manage, :all
elsif user.role? :moderator
can :manage, Post
else
can :read, :all
end
end
end
# posts/index.html.haml
...
- if can? :update, #post <- doesn't work
- if can? :update, Post <- works
Edit: add PostsController.rb
#posts_controller.rb
class PostsController < ApplicationController
before_filter :login_required, :except => [:index, :show]
load_and_authorize_resource :except => [:create]
def index
# #posts = Post.all ## <- handled by Cancan's load_and_authorize_resource
#events = Event.where("end_date <= :today", :today => Date.today)
#next_event = Event.next
respond_to do |format|
format.html # index.html.erb
format.json { render json: #posts }
end
end
...
end
This line:
- if can? :update, #post <- doesn't work
Is asking CanCan "can I update this specific post." You defined the ability in terms of all posts. If you had done:
can :update, Post, :user_id => user.id
Then your "if can?" would work, and the user would only be able to update their own posts. So you want to use the specific resource version ("#post") if something about this instance of the resource determines the permission, and you want to use the class version ("Post") if the user has the ability for all instances of the class.

Rails & CanCan: If user is logged in then allow him/her to view index page?

I am using authlogic and cancan on a rails 3 application, I want to allow all logged in users to access the users index page, i have tried something like this but it dosent seem to be working:
ability class:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can :index, User if UserSession.find
can :read, User if UserSession.find
end
Controller:
def index
#users = User.search(params[:search]).order('username').page(params[:page]).per(1)
authorize! :index, #users
end
def show
#user = User.find(params[:id])
authorize! :read, #user
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #user }
end
end
thanks
I find it's easier to use load_and_authorize_resource at the top of my controllers. Then your ability class contains all the ability logic instead of having it strewn about your controllers.
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user
can :index, User
can [:show, :edit, :update, :destroy], User, :id => user.id
end
end
end
users_controller.rb
class UsersController < ApplicationController
load_and_authorize_resource
def index
#users = User.search(params[:search]).order('username').page(params[:page]).per(1)
end
def show
end
...
end
I haven't used authlogic in a while as I tend to use devise now, so I'm not sure if my sample code is authlogic ready. If you don't want to use load_and_authorize_resource, my code shows how to limit what users can see in the ability class, but in your code I'd change :read to :show.
Continuing from my comment, the problem was in the following code
authorize! :index, #users
Here, you're passing an Array of users to the CanCan's method, while your can :index, User declaration defines the authorization for a User object.

How to obtain action level protection using Authlogic and STI?

Given that it is well-documented how to use before_filter for a single user classification, I'm having trouble getting action-level protection for multiple user types. Let me explain:
I've got something like this...
class ApplicationController < ActionController::Base
class << self
attr_accessor :standard_actions
end
#standard_actions = [:index, :show, :new, :edit, :create, :update, :destroy]
def require_guardian
unless current_user and current_user.is_a?(Guardian)
store_location
redirect_to home_url
return false
end
end
def require_admin
unless current_user and current_user.is_a?(Administrator)
store_location
redirect_to register_url
return false
end
end
end
And in the GuardiansController I want to only allow the standard actions for Administrator but all other actions should require Guardian. So I tried this...
class GuardiansController < ApplicationController
before_filter :require_admin, :only => ApplicationController::standard_actions
before_filter :require_guardian, :except => ApplicationController::standard_actions
...
end
Which ends up doing a recursive redirection. There must be a better way?
OK, this is another case of not looking carefully and missing something. I inadvertently had setup the route to redirect the user in a recursive way. The above solution works just fine when you set the routes properly:
def require_guardian
unless current_user and current_user.is_a?(Guardian)
store_location
# this route (home_url) sent the user to another controller which ran a before_filter sending them back here again.
# redirect_to home_url
# So I changed it to a neutral route and it works great!
redirect_to register_url
return false
end
end

Resources