I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform
Related
I am a little stuck with Pundit: It feels that the solution should be easy - but I am not getting it. Actually I have a bunch of models which are all dependent on one main model. My main model is a script. The script has many roles, many scenes, many costumes, etc. Additionally it has some joined connections like scene.roles. I simply want to authorize the user who created the script to do everything (adding roles, deleting scenes, just everything that is in the scope of her own script) and everybody else to do (and see) nothing. Do I need to create a policy for every model or can I just (re-)use somehow one "script policy"?
How would an authorization look like in a dependent controller (i.e. 'index' in roles or 'new' in scenes)?
The authentication is handled by Device. A user must be logged in to see or do anything.
This is my first post on stack overflow, happy to join the community:-)
Pundit policies are POROs (plain old ruby objects). So you can easily create a policy for the main model:
class ScriptPolicy
attr_reader :user, :script
def initialize(user, script)
#user = user
#script = script
end
def update?
user.actor?
end
def delete?
user.admin?
end
end
And then for every model that you have, simply create an empty class definition that inherits from the ScriptPolicy
class ScenePolicy < ScriptPolicy
end
Another approach would be to overwrite the policy name that Pundit is referencing directly in the child model. So assuming that you have a model Scene that shall use the same policy as Script, you can add a method:
class Scene < ActiveRecord::Base
def self.policy_class
ScriptPolicy
end
end
In the latter, you do not need to create empty policy classes for every model, but you trade it for the decreased flexibility in defining specific permissions for models.
When calling the authorize method in your controller, you can specify the policy class:
authorize #model, policy_class: ScriptPolicy
I find that this solution generates less boilerplate files.
To authorize a scope:
scenes = policy_scope(Scene, policy_scope_class: ScriptPolicy::Scope)
By the way, this is all covered in pundit's README: https://github.com/varvet/pundit
I am trying to add current user information to record using ActiveRecord callback, but I don't see way how to do that.
I tried Thread.current[:user], but in results I see that thread value is accessed from another user sessions.
I am using Passanger in production, but in the same time I am using acts_as_audited who get user value correctly.
Whats the best/safest way how to do that?
The current user is not accessible from within an ActiveRecord model. This is due to the separation of concerns in Rails--the current user and the session are concepts that pertain to the realm of the controller.
You need to get the relevant data and pass it into the model in order for your callback to work. One way to do this is with an accessor method:
# model
attr_accessor :current_user
def my_callback
# do something with current_user
self.some_attribute = current_user
end
# controller
#model = MyModel.find(params[:id])
#model.current_user = current_user # assuming you have a controller method that does this
#model.save!
You should rename the current_user accessor to be meaningful: eg. if you are tracking the author of a blog post, call it author or responsible_user or some such, since current_user means nothing once the model is saved.
I'd like to use pg_audit_log for logging in a rails app. The audit log must not only show the columns that have changed, but also the user who made those changes. The docs don't show how to do this, but after looking through the pg_audit_log source (postgresql_adapter.rb) I see it reads user information from a thread local variable, ala:
current_user = Thread.current[:current_user]
I've considered setting/unsetting this in before and after filters like so:
Thread.current[:current_user] = current_user
(using the current_user helper method in the controller to get the currently logged in user), but that seems dangerous. I'm now spending time trying to understand how the rails request cycle and threads interact, to get a better feel for just how dangerous. In the mean time, I was curious if any SO users currently using pg_audit_log have solved the need to log the user_id and user_unique_name to the log tables each time the user makes a change to a record.
Setting the current user the way you describe is a common way to do it. See, for example, http://rails-bestpractices.com/posts/47-fetch-current-user-in-models
Some example code could look like:
# in your model
class User < ActiveRecord::Base
def self.current
Thread.current[:current_user]
end
def self.current=(user)
Thread.current[:current_user] = user if user.nil? || user.is_a?(User)
end
end
# in your controller
class ApplicationController < ActionController::Base
before_filter :set_current_user
def set_current_user
User.current = user_signed_in? ? current_user : nil
end
end
Relying on the Thread.current hash to provide model-level access to objects managed by the controller is indeed controversial. For example, see the following:
Safety of Thread.current[] usage in rails
It is worrisome that this particular feature is undocumented in the pg_audit_log gem.
Suppose you had not actively explored the gem's source code, and suppose you had independently decided to define Thread.current[:current_user] = something in your own application, for your own purpose. In that case, pg_audit_log would audit that object, without your knowledge.
Granted, the name current_user is so universally accepted to mean the currently logged-on user as defined by authentication routines that it's difficult to imagine this potential bug as a concrete problem, but from a design standpoint? Ouch.
On the other hand, since you know what you are doing, ensuring that Thread.current[:current_user] is set/unset at the beginning/end of each and every response cycle should make the process safe. At least that's what I gather from reading lots of posts on the topic.
Cheers, Giuseppe
I have a Rails application where a user longs in and I have the user_id in the session. The next step is to create a scope for all model data shown to the user where data.user_id = session[:user_id].
I know I can do the following in each of my controllers
Controller.find_all_by_user_id(session[:user_id])
Yet to me it seems there is probably a better solution. I found the possibility to add a scope to the model, yet the session is not known here and MVC pattern wise it is probably not a good idea to have it there. Is there a solution to apply such a user_id restriction to all data coming from the models or should I just use the find_all_by_user_id for every controller function that has userdata in it?
If I understand you correctly you want to access some data by user_id. Which means that you can define relationship in the user model as has_many :this_and_that or something like that. It it is right, then you can create a before_filter or even better a function in your application controller in which you get your current user instance. Trough this instance, you can access all available data to that user. You can even make that function a helper function, and you can use that in a view.
#User.erb
has_many :other_data
#ApplicationConroller.erb
def current_user
#current_user ||= User.find_by_id(session[:user_id])
end
#OtherControllers
def index
#other_datas = current_user.other_datas
end
You can have before_filter :load_user_data, :if => current_user or something like that.
Method will look like that:
def load_user_data
#data = ModelName.where(user_id: session[:user_id])
end
#data will be ActiveRecord::Relation and will be chainable. Also using find_all_by_user_id in each controller not that bad, if you really need it.
I have a permission model in my app, that ties (Users, Roles, Projects) together.
What I'm looking to learn how to do is prevent a user for removing himself for their project...
Can you give me feedback on the following?
class Permission < ActiveRecord::Base
.
.
.
#admin_lock makes sure the user who created the project, is always the admin
before_save :admin_lock
def before_save
#Get the Project Object
project = Find(self.project_id)
if project.creator_id == current_user.id
# SOME HOW ABORT OR SEND BACK Not Allowed?
else
#continue, do nothing
end
end
end
Is that look like the right approach?
Also, I'm not sure how to do the following two things above:
How to abort prevent the save, and send back an error msg?
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Thanks for reading through
How to abort prevent the save, and send back an error msg?
return false during the callback chain tells activemodel to stop (similar to how adding errors to the model during a validation tells it to stop at that point)
self.errors.add_to_base "msg" will add an error to the model, which can then be rendered on the view.
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Models shouldn't really know about things like the current request, if at all possible, you should be locking things down at the controller/action level.
EDIT:
So, the role of controllers is to deal with everything involved in getting the correct information together based on the request, and passing it to the view (which becomes the response). People often say "make your models fat and your controllers skinny", but that could be said of any system that embraces object oriented design -- your logic should be in objects when possible.
That being said, the whole point of controllers is to deal with routing the right things to the right places, and authentication is definitely a concern of routing.
You could easily move the line comparing creator_id to user id in the action, and react based on that.
Now, sometimes you genuinely need that stuff in the model and there is no way around it. That becomes a problem, because you need to fight rails to get it there. One way would be to attr_accessor a current_user field on your model, and pass that in on initialize. Another would be to remove the fields from the params hash that a user is not allowed to change in the action. Neither is really that nice though.
Agreed with Matt that you should try to use the controller for the redirect. The model should have the logic to determine if the redirect is appropriate. Maybe something like
class ProjectsController < ApplicationController
def update
redirect_to(projects_url, :alert => "You can't remove yourself from this project.") and return if Role.unauthorized_action?(:update, params[:project])
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
...
end
class Role
def self.unauthorized_action?(action, params)
# your logic here
end
You should check out CanCan for some ideas.
In permission model take one field project_creater as boolean
In project modelbefore_create :set_project_ownership
def set_project_ownership
self.permissions.build(user_id: User.current.id, project_creater: true)
end
In project controllerbefore_filter :set_current_user
In Application controllerdef set_current_user
User.current = current_user
end