ruby on rails way to destroy extra session variables in a model? - ruby-on-rails

I have additional variables I add to an authlogic users session like:
session[:current_profile] = extra_id
I currently destroy these on logout in a controller like:
session[:current_profile] = nil
I'd like to clean this up and destroy them in the session model in the after_destroy method like:
def after_destroy
session[:current_profile] = nil
end
This session method doesn't seem to be callable from the models though. Any idea how to destroy a session variable from a model?
Thanks!

You're really not supposed to alter things in the Controller space from the Model space, which is to say, a Model should not be controlling a Controller. Models should be able to run independently of a controller, such as in unit tests where no controller is present.
While you might be able to wrangle this sort of thing with an Observer, I don't know of an easy way to do this. It's probably better to have the controller perform all the required actions directly.
If you put an after_destroy hook like this in you will have some serious side-effects if, for example, a user logged in as an admin destroys another user account and then their session profile suddenly disappears.

Related

How does cancancan set the model instance, and how can I inspect it?

I noticed users could access an action they shouldn't be able to access.
I debugged in the rails console with something like
user = User.first
physician = Physician.first
ability = Ability.new(user)
ability.can?(:send_message, physician)
# => false
The above says that user can't access the send_message action for that physician, which is the desired behaviour, yet I know they can in the app!
I think this narrows down the cause to a problem with cancancan loading the wrong model instance for some reason. And that's hinted to in the cancan docs too:
Note: this assumes that the model instance is being loaded properly.
But the problem is I'm not sure how to diagnose the problem from here, since the console says it should be working. I don't know how to view the model instance that cancancan has set, and I don't know what else to try.
Any ideas?
Update
I did manage to work around this by using authorize! :send_message, physician in the controller, but since I only stumbled upon this behaviour by chance, I think it's much more important to figure out why the wrong model instance was being loaded (especially so I can see if that was happening elsewhere too).
I figured out why it was probably happening (but I still don't know how to disagnose it)
I think this was happening because I had many custom actions, and some had #physician = Physician.find(current_user.physician.id) (i.e they're the current user), whereas others were more like #physician = Physician.find_by_id(physician_params[:id]). I'm not sure how cancan sets the instance model, but I do know it's not psychic, so it wouldn't know whether to set it to the current user's physician instance, or the physician instance for the physician id passed in.
What remains?
How does cancancan set the model instance for custom methods (I presume it tries something, and if that doesn't work, tries something else, etc etc)?
Small notes that help:
load_and_authorize_resource does attempt to load the model instance for non RESTful actions
Some useful info in the docs
This may have something to do with what I experienced:
When I returned slug it breaks this behaviour and I can edit all pokemons.
Leaving my notes here in case they are helpful to anyone else.
TL;DR, there are a lot of nuanced assumptions cancancan makes, which you won't know about from the outset. I discovered many of them by thoroughly reading the comments in the cancancan readme, code, and defining abilities docs
So here goes..
How cancancan works
if you call authorize! in the controller action itself, cancancan will look for an instance variable in each controller action.
if you instead simply add load_and_authorize_resource at the start of your controller, that will do two things:
Load an instance variable that cancancan thinks should be loaded, and
Checks for authorization on that model instance
Note that for custom actions, load_and_authorize_resource will still try to load a model instance, but how does it know what to load? It doesn't, it guesses, which, for me, I do not like, so be aware of that.
For me, I prefer to do the work of load_and_authorize_resource myself in two separate steps, so I know exactly what's going on.
Ensure #article is generated via a before action for each controller action (or #articles for index action)
Simply have a line at the top of the controller saying load_and_authorize_resource after the before action that sets the model instance
Note that the only difference is now the developer is responsible for loading the right model instance, and cancancan is not trying to guess it. I prefer this approach because it only takes one mistake to accidentally allow access where it shouldn't be granted.
Also remember that load_and_authorize_resource should always go after any before actions that set the model instance variable
Random notes that may also help
The name of the instance variable depends on the action. If we have an articles controller, then:
For the index action, authorize looks for #articles
For all other actions, authorize looks for #article
It then checks to see if the user is allowed access to that resource.
load_and_authorize_resource checks to see if the model instance exists, and if not, creates one. So if you have a before action that creates #article/#articles, then load_and_authorize_resource won't do it for you (i.e. it won't overwrite it), but if you didn't set one, cancan will try to set one. See here for more on that.
An ability rule will override a previous one. (see here for an example)
Just one last thing, never use current_user in ability.rb, it will error silently (!!), so be sure to use user instead :)
Here's what is happening: https://github.com/CanCanCommunity/cancancan/blob/585e5ea54c900c6afd536f143cde962ccdf68607/lib/cancan/controller_additions.rb#L342-L355
# Creates and returns the current user's ability and caches it. If you
# want to override how the Ability is defined then this is the place.
# Just define the method in the controller to change behavior.
#
# def current_ability
# # instead of Ability.new(current_user)
# #current_ability ||= UserAbility.new(current_account)
# end
#
# Notice it is important to cache the ability object so it is not
# recreated every time.
def current_ability
#current_ability ||= ::Ability.new(current_user)
end

How can I allow only a specific user role to update an attribute to a specific value

Afternoon, got a bit of an issue I am not sure how to resolve.
I am trying to setup some rules that allows only certain types of user roles to update the status attribute on a model to a certain status.
So I looked into doing this with pundit as it seems to be an authorisation issue, however one problem with that is you cannot pass the params to the pundit policy which I would need access too (so I can see what attribute they are trying to change to), and it seems that its bad practise to pass params to a pundit policy.
The next option was to make it a callback in the model, however the problem here is I don’t have access to the current_user inside the callback and again it seems its bad practise to add the current_user helper into a model.
So I am left with perhaps doing it in the controller? Again does not seem the right place for it?
An example to make it a little easier to understand:
I want to allow a User with the role of admin to be allowed to change the status of a post to "resolved", no one else is allowed to change the status to "resolved"
Try this,
create a instance method in User model like bellow,
def is_admin?
self.has_role(:admin) # if you are using rolify
---OR---
self.status == "admin" # if you have status attribute in your user table
end
Then call this method on current_user in edit/update method of post controller. to check current_user is admin or not

Setting Pundit role for user from Devise Registrations New View / Controller

I have both Pundit and Devise setup and working correctly in my rails app. However I am unsure about how to let the user decide their role when signing up.
At the moment:
I have a URL param which is passed to the Devise new view.
In the form_for I set a hidden field called role to the value of the param.
This works.
But I am concerned that a malicious user could change this param to say "Admin" and now they are an admin.
How should I handle this? I don't want to put a restriction in the model as that will cause issues when I want to create an admin. Should I override the devise registrations controller to put a check in there?
You don't need to override Devise's RegistrationsController for what you're trying to do.
If you want admins to be able to create users that have an arbitrary role set, you could simply use your own controller. Devise still makes it easy to create a user yourself, so you'll just have to make a controller handling this. Of course, don't forget to protect it using Pundit so only admins can use this functionality.
This approach still works if you use the Confirmable module. As no confirmation e-mail will be sent on user creation, though, you'll either have to call user.confirm! after saving the model to immediately unlock the account, or manually send the confirmation e-mail using user.send_confirmation_instructions.
Edit:
This Pundit policy may or may not work for what you're trying to do. You will have to override the create action of Devise's RegistrationsController here in order to use Pundit's authorize method. For dryness' sake, you should also move the roles list elsewhere, perhaps into the model.
class UserPolicy < Struct.new(:current_user, :target_user)
def create?
registration_roles.include?(target_user.role) if current_user.nil?
end
private
def registration_roles
%w(RED BLU Spectator)
end
end
After a fair amount of googling I have an answer. First stick some validation in your model for the roles Active Record Validations Guide: See 2.6 inclusion: validator option
After this your roles are validated to ensure they are correct, you could of course have a lookup table as well. Then you have two options:
Use a conditional before_save Callback for new records. Then check if the new record has the role your protecting and if so raise an error. To catch later (in an overridden devise controller (see second option).
Override the Devise registrations controller See this SO question. And put some checks in a completely overridden create action. Use the session to store the url param passed to the new action (also needs to be completely overridden). Then if the create action fails and redirects to new you still have access to the role in the session (as the param will be cleared from the URL unless you manipulate it).
So either way you need to override the registrations controller, its just a case of how much.
I suspect there is a way to do this with just Pundit. But I have yet to be able to get it to work.

Rails - Expiring Cached Actions from Resque (a Delayed Job alternative)

I have Resque queue that processes a particular model to do some post processing. Once this model is updated I would like to expire the cached view action.
I have a Sweeper setup and working, but it only observes changes made to the models in Controller Actions.
I know it is not very MVC to be expiring cached items from my model, but all my post processing logic is contained in my models.
Ideally I would like to not double up on my cache busting code, so if I could get my existing sweeper to watch model changes that would be ideal.
Alternatively I would settle for expriing the action cache from inside my model OR a model observer.
p.s.: I can expire cached fragments from within a model observer I have setup, but not actions.
I'd really like to know what the best practice is here. I'm sure I am not the only one who has this requirment.
Thanks.
I'm wondering if something like this is the way to go:
http://dev.mensfeld.pl/2011/07/rails-wykorzystywanie-sweeperow-poza-kontrolerami-na-samych-modelach/
Since I wasn't able to add my comment on that site you linked to. I'll put it here:
In Rails 3.1.3, if you instantiate the controller and then try calling expire_fragment, you'll get errors about trying to call host on NilClass.
After some experimenting, I remembered that functional tests can instantiate your controller. So I changed the instantiation code to:
#controller ||= ApplicationController.new
if #controller.request.nil?
#controller.request = ActionDispatch::TestRequest.new
end
This seems to work, even in production, even using rails console.
In the end I came up with the following solution:
Added my existing sweeper to the list of object observers in application.rb:
config.active_record.observers = :app_sweeper
Added code to the sweeper methods to instantiate the #controller object if it was missing in the sweeper (which is what happens when coming via a object instead of a controller).
#controller ||= ActionController::Base.new
I could then use the expire_fragment method to expire the cached action, with the following tidbit.
expire_fragment("#{ActionMailer::Base.default_url_options[:host]}/items/#{item.id}")
That said, mj1531's answer might prove to be a nicer solution if it means that I can use the expire_action method instead of faking it with the expire fragment. I will report back when I have test it out and select the best answer.

How do I store an instance variable across multiple actions in a controller?

Say I want to store some variable in my controller. I want to initialize it in one action, increment it in another, and read it in yet another. Just declaring this variable with #foo doesn't work because #foo dies after the action that created it is rendered.
I do not want this variable to be stored in a model.
Is there a way to preserve this variable besides storing it in a session?
It seems like I've run into this simple problem a few times, and I want to know the best way to go about solving it.
Not really. Each call to a controller action is stateless. Nothing is available after the controller action finishes. A new controller instance is created for each request, and then discarded at the end of the request.
If you don't want to store it in the session, or database model, you don't have many options if you're wanting that variable to be specific to a particular session.
If it is global across all sessions, you could put it in a ##class_variable rather than an #instance_variable, but that can get messy once you start having multiple Rails processes (each which will have their own copy of it), or if you're running in threadsafe mode, you can end up with nasty concurrency bugs.
I guess you could look at something like memcached, but you'd still need to key that to some user_id or other session marker (unless it's global)
I too am wondering why you are against using session? If you don't like working with session directly in your actions, you could emulate a surviving #foo instance variable with filters. Something like this maybe?
class FooController < ApplicationController
before_filter :load_foo
after_filter :save_foo
private
def load_foo
#foo = session[:foo] || 0
end
def save_foo
session[:foo] = #foo
end
end
Your actions will the be able to manipulate the value through the #count instance variable and this will be automatically persisted to session.
You could make use of the built in Rails.cache mechanism to store the value but as mentioned in the first answer you'd have to key it off something like the user_id. This is a nice way to go since you can back it with different storage mechanisms.
Rails.cache.write(:foo)
# in later action
Rails.cache.read(:foo)
One other thing you could look at is the flash hash, which provides a keep method to make the flash value last more than one subsequent request.
So in action 1 you could create the value:
flash[:foo] = some_value
flash.keep(:foo)
In action 2 you can access it, and call keep again if you want it to stay alive for more subsequent actions.
flash[:foo] #use it for something
flash.keep(:foo) # keep it for another request
It's a bit of a tricky thing to do cleanly within the context of http requests.
If it's a simple count or string, I think the best solution is to store it in the session. That way it will be there if you are using multiple web servers.
Why are you against using a session for this?
Don't worry, sessions won't bite.
Also, the session is probably the best way to do this.

Resources