Rails - Flow control question, is there a better way? - ruby-on-rails

I am trying to lock-down a few controllers based on role and the 'posts' controller by whether or not they ANY permissions assigned. This appears to be working, but I'm wondering if there is a clean way to handle this. This is what I have in the application controller, which I'm calling as a before filter...
if controller_name == 'users' || 'accounts'
unless #current_user.master? || #current_user.power?
render :template => "layouts/no_content"
end
elsif controller_name == 'posts'
unless #current_user.permissions.count > 0
render :template => "layouts/no_content"
end
end
Thanks in advance.

You shouldn't make a code snippet that checks for a controller name to take a specific action in application.rb. You should define that before filters only in the controllers that need them
Make 2 methods in ApplicationController:
private
def require_master_or_power_user
unless #current_user.master? || #current_user.power?
render :template => "layouts/no_content"
end
end
def require_some_permisions
unless #current_user.permissions.count > 0
render :template => "layouts/no_content"
end
end
Now add this as a before filter where you need it:
class UsersController < ApplicationController
before_filter :require_master_or_power_user
...
end
class AccountsController < ApplicationController
before_filter :require_master_or_power_user
...
end
class PostsController < ApplicationController
before_filter :require_some_permisions
...
end
So the ApplicationController defines the filters, but its up to your other controllers whether or not to actually use those filters. A superclass like the ApplicationController should never conditionally branch its execution based on its subclasses. Choosing when to use the provided behaviours are one of the reasons why you want to subclass in the first place.
It's also much clearer from a code readability standpoint. When looking at the UsersController, its immediately obvious there is some permission stuff happening when you see a before filter with the name like "require_something". With your approach, you can't tell that from looking at the users controller code itself at all.

I would strongly suggest you adhere to MVC and OOP and move as much of the user related logic back into the User model like this:
class User < ActiveRecord::Base
def has_permission?
true if self.master? || self.power? || (self.permissions.count > 1)
end
then you could just use one filter in application.rb:
protected
def check_template
render :template => "layouts/no_content" if current_user.has_permission? == true
end
and call that with a before_filter as suggested by Squeegy, either in the respective controllers, or site wide in application_controller.rb
before_filter :check_template
This approach is obviously a little cleaner and a lot less brittle if you ever decide to change the scope of what gives people permission, you only have to make one change application wide.

I would advise that you use an ACL system for this: http://github.com/ezmobius/acl_system2

A short little handwritten DSL. Haven't even checked the code for syntax errors, but you'll get the picture. In your application controller:
before_filter :handle_requirements
def self.requirement(*controllers, &block)
#_requirements ||= {}
#_requirements[controllers] = block
end
def handle_requirements
return unless #_requirements
#_requirements.each do |controllers, proc|
if controllers.include?(controller.controller_name)
restrict_access unless instance_eval(&block)
end
end
end
def restrict_access
render :template => "layouts/no_content"
end
Usage (also in your application controller)
requirement('users', 'accounts') do
#current_user.master? || #current_user.power?
end
Or, just use the ACL system Radar mentions.

Another plugin worth a look is role requirement, which I've been using. I think they can both do roughly the same things.

Here is a plug for RESTful_ACL; an ACL plugin/gem I've developed, and is being pretty widely used. It give you freedom to design your roles as you see fit, and it very transparent.

Related

How can I customize the way Rails 4 render() finds files?

I'm serving a versioned web service from Rails.
I would very much like to be able to call render like normal:
render 'index'
And have it correctly serve the requested version from the following:
index.v1.json.jbuilder
index.v2.json.jbuilder
index.v3.json.jbuilder
Assuming that I already know the requested version within the context of the controller action execution, how do I get render() to leverage it?
I have used the versioncake gem
You should definitely check this out. File name will be very close to what you have:
index.v1.json.jbuilder
would be
index.json.v1.jbuilder
Sounds like a builder design pattern might work here. Have a view builder object that returns the desired behavior.
module ViewBuiler
def build(name, api_version)
View.new(name, api_version).build
end
class View < Struct(:name, :api_version)
def build
[name, api_version, 'json', 'jbuilder'].join('.')
end
end
end
and in your controller you could just do something like:
ApplicationController
include ViewBuilder
end
MyController < ApplicationController
def index
...
# you can pass either strings or symbols to build and it will
# return 'index.v1.json.jbuilder'
render build(:index, params[:api_version])
end
end
And disclaimer, this is just an idea and not something I've implemented. I like the approach for 2 reason. The Controller actions remain skinny and don't have logic in it. A builder like this seems pretty easy to test. And it keeps the thing that might change (views etc) isolated into something that can change frequently as long as it retains it's interface that the Controllers will work with.
This seems like a candidate for Variants. This is new to 4.1 though.
class MyController < ActionController::Base
before_action :set_variant
def my_action
.....
respond_to do |format|
format.json do |json|
json.v1 do
# render whatever you need here
end
end
end
end
protected
def set_variant
request.variant = :v1 if request.params[:version] == "v1"
....
end
end

Authorize with multiple action in CanCan

I am trying to understand a bit better the capabilities of CanCan when it comes to authorization. Imagine this controller action:
def update
if can? :action, Model or can? :resolve, Model or can? :authorize, AnotherModel
# My Code here
respond_with #model
else
raise CanCan::AccessDenied.new(nil, :update, Model)
end
end
I got to this point while trying to find a solution to the above using authorize!. As far as I can see (also looking at the signature) authorize! only accepts one permission (action) and one subject, with an optional message, like this:
def authorize!(action, subject, *args)
# code
end
Is there a way which I may be overlooking to instruct authorize to check for multiple actions? Putting two authorize one after the other will act as an AND condition between permissions, what I would like is it to work like an OR condition, basically similar to the custom code above (which has the problem of raising the AuthorizationNotPerformed in CanCan, avoidable with skip_authorize_resource which is not something I would really like to do).
You can create an custom action and create as many or-conditions as you like.
can :my_update_action, Project do |project|
can?(:read, ModelX) || can?(:read, ModelY) || ...
end
In the end I added this rather nice solution to the ability class:
def multi_authorize!(*actions, message_hash)
message = nil
if message_hash.kind_of?(Hash) && message_hash.has_key?(:message)
message = message_hash[:message]
end
auth = false
actions.each do |act|
auth = auth || can?(act[0], act[1])
end
if !auth
message ||= unauthorized_message(actions[0][0], actions[0][1])
raise CanCan::AccessDenied.new(message, actions[0][0], actions[0][1])
end
end
Included an helper for the Controllers:
module CanCanAddition
def multi_authorize!(*args)
#_authorized = true
current_ability.multi_authorize!(*args)
end
end
if defined? ActionController::Base
ActionController::Base.class_eval do
include ApplicationHelper::CanCanAddition
end
end
Which I call like this:
def create
multi_authorize! [:create, Model1], [:authorize, Model2], :message => "You are not authorized to perform this action!"
# other code...
end
WARNING: Due to the code in the ability class, you must provide a message or the last pair of authorization will not be passed in the *args. I'll take some time to overcome this but the idea of the solution I think fits nice with.

Rails: Verify correct user across multiple controllers

I have several controllers that require a correct user for their edit/update/delete actions. What is the Rails-way to accomplish the following:
Currently, in each controller I have the following code:
class FooController < ApplicationController
before_filter :correct_user, :only => [:edit, :update, :destroy]
# normal controller code
private
def correct_user
#foo = Foo.find params[:id]
redirect_to some_path unless current_user == #foo.user
end
end
I have similar code in 3 controllers. I started to bring it out to a helper like this:
module ApplicationHelper
def correct_user( object, path )
if object.respond_to? :user
redirect_to path unless object.user == current_user
end
end
But I'm wondering if this is a good way to do it. What's the accepted way to solve this?
Thank you
EDIT
The correct user check here is because I want to make sure it's only the author who can make edits/deltes to each of the objects.
To clarify, the objects would be things like Questions and Posts. I don't want to use something like CanCan as it's overkill for something simple like this.
I really like using RyanB's CanCan, which allows you to both restrict access to actions based on the user, and centralize such authorization into basically a single file.
CanCan on GitHub: https://github.com/ryanb/cancan
Screencast explaining how to setup/use it: http://railscasts.com/episodes/192-authorization-with-cancan
EDIT
No problem. I hear you on CanCan - it takes a little while to get up and running on it, but it's designed to do exactly what you're asking - per object authorization.
Alternative:
Another way to do this is move your authoriship/current_user check to the ApplicationController class, from which all of your other Controllers inherit (so they will get that code through inheritance - and you don't need to write the same code in multiple Controllers), and it would look something like...
class ApplicationController < ActionController::Base
...
helper_method :correct_user
private
def correct_user( object, path )
redirect_to path unless object.user == current_user
end
end
You should do the following :
def edit
#foo = current_user.foos.find(params[:id])
end
This way, only if the current user is the owner of the Foo he will be able to see it.

attributes and constructors in rails

I'm new to rails and don't even know if this is the correct way of solving my situation.
I have a "Club" ActiveRecords model which has a "has_many" association to a "Member" model. I want the logged in "Club" to only be able to administrate it's own "Member" so in the beginning of each action in the "Member" model I did something similar to the following:
def index
#members = Club.find(session[:club_id]).members
to access the right members. This did not however turn out very DRY as I did the same in every action. So I thought of using something equivalent to what would be called a constructor in other languages. The initialize method as I've understood it. This was however not working, this told me why, and proposed an alternative. The after_initialize.
def after_initialize
#club = Club.find(session[:club_id])
end
def index
#members = #club.members
....
does not seem to work anyway. Any pointers to why?
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.members
Makes me think that the #club var isn't set at all.
Also, is this solution really a good one? This makes it hard to implement any kind of "super admin" who can manage the members in all of the clubs. Any ideas on where I am missing something?
You can use a before_filter.
Define the filter in your ApplicationController (so that you can access it from any controller).
class ApplicationController < ActionController::Base
# ..
protected
def load_members
#members = if session[:club_id]
Club.find(session[:club_id]).members
else
[]
end
end
end
Then, load the filter before any action where you need it.
For example
class ClubController < ApplicationController
before_filter :load_members, :only => %w( index )
def index
# here #members is set
end
end
Otherwise, use lazy loading. You can use the same load_members and call it whenever you need it.
class ClubController < ApplicationController
def index
# do something with members
load_members.each { ... }
end
end
Of course, you can customize load_member to raise an exception, redirect the client if #members.empty? or do whatever you want.
You want to use a before_filter for this.
class MembersController < ApplicationController
before_filter :find_club
def index
#members = #club.members
end
private
def find_club
#club = Club.find(session[:club_id])
end
end
I'm a fan of a plugin called Rolerequirement. It allows you to make custom roles and apply them by controller: http://code.google.com/p/rolerequirement/

How do you implement a function which returns the URL of last visited page

I want to store the current URL in a session variable to reference the previous visited page.
If I store every URL (via a before_filter on ApplicationController), also actions which end in a redirect (create, update, etc) are considered as last visited page.
Is there a way to tell Rails only to execute a function if a template is rendered??
Update
Thanks for the after_filter tip... having written so many before_filters I didn't see the obvious. But the Trick with #performed_redirect doesn't work-
This is what I got so far
class ApplicationController < ActionController::Base
after_filter :set_page_as_previous_page
def set_page_as_previous_page
unless #performed_redirect
flash[:previous_page] = request.request_uri
else
flash[:previous_page] = flash[:previous_page]
end
end
end
I need to implement a "Go Back" Link, without the use of Javascript, the HTTP Referer. Sorry If I should have mentioned that, I appreciate your help!
Update 2
I found a solution, which is not very elegant and only works if your app follows the standard naming scheme
def after_filter
if File.exists?(File.join(Rails.root,"app","views", controller_path, action_name+".html.erb"))
flash[:previous_page] = request.request_uri
else
flash[:previous_page] = flash[:previous_page]
end
end
Not sure why #performed_redirect isn't working, you can see that it does exist and have the desired values by calling the actions on this test controller:
class RedirController < ApplicationController
after_filter :redir_raise
def raise_true
redirect_to :action => :whatever
end
def raise_false
render :text => 'foo'
end
private
def redir_raise
raise #performed_redirect.to_s
end
end
As an aside, instead of doing
flash[:previous_page] = flash[:previous_page]
you can do
flash.keep :previous_page
(My patch, that. back in the days :P)
Another possible approach to determine whether the response is a redirect vs. render is to check the status code:
class ApplicationController < ActionController::Base
after_filter :set_page_as_previous_page
def set_page_as_previous_page
unless 302 == request.status #redirecting
flash[:previous_page] = request.request_uri
else
flash[:previous_page] = flash[:previous_page]
end
end
end
It really seems like there should be a redirect? method in ActionController::Base for this.
The controller will have these variables, which might be helpful:
#performed_render
#performed_redirect
But anyway, how exactly are you storing the url? Show us the filter code. Why not using an after_filter?
Can be a bit more specific? I cant get your question- by template you means render :view, the layout? or only when called with render :template?
Rendering a page? render :action=>:new is a page too... Can you be a bit more specific on which you want to capture and which you want to exclude?
Saw the accepted answer :)

Resources