In my application I have a header that has many rules, since I have many types of profiles.
I want to make just one query (maybe two) to get the user and make all the checks that I need
In my application controller I have this method:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
To check the current user, but I keep calling it on my header.html.erb, if I check it, like, 10 times, I will do 10 queries...
As far as I know when I first call current_user I would get a variable #current_user to use. But this is clear for me when I have a controller. I call current_user and on my view I just check #current_user.something. since /layouts/_header.html.erb doesn't have a controller, how can I do this ?
What you have should not perform multiple queries if the lookup is successful -- it will set #current_user to the return value of the query and then just return that in the future. However, if no user is found, #current_user will be initialized to nil and so additional calls to current_user will execute the query again.
One possible workaround is to used the defined? operator like so:
def current_user
return #current_user if defined?(#current_user)
#current_user = User.find_by_remember_token(cookies[:remember_token])
end
However I don't love this implementation, because if anything defines #current_user in the current closure, then this will return the value whether the lookup has been done or not. You could also set another variable that tracks whether the current user has been looked up and only execute the query if it has, but that seems ugly to me, too.
For a more robust solution to this general problem, see the memoist gem. Or better yet, have you considered whether devise and/or warden might be right for your authentication needs?
since /layouts/_header.html.erb doesn't have a controller, how can I do this ?
Your layouts do have controllers. It's impossible for them to be rendered otherwise.
What you're doing now will work as it's written, and it will minimize the number of queries, and your layout files will have access to the current_user helper, provided you've made it available to your views via helper_method :current_user in your controller.
There's really nothing wrong with your code, whatsoever; all the problems you think you're having, you're probably not really having.
From the following
" I have a header that has many rules, since I have many types of profiles"
I guess you are verifying multiple conditions based on user role. If my assumption is correct then
Better create separate partials for each role.For example if you have admin role then
create partial in layouts/_admin.html.erb
Then you can include it as
<%= render "layouts/" + current_user.role %>
Related
I have used declarative authorization gems like cancan and cancancan in the past to control access to data. However, in this new application I am trying to control access to actual features.
It's your typical SaaS model, where progressively more features are available depending upon the level at which the user has subscribed.
Most of the features are available by clicking different icons or menu items throughout the application.
I'm writing to inquire whether there is a well-known implementation pattern for this that I'm not aware of before I roll my own.
Here are the different types of things that will be limited based upon the subscription level:
Features accessible by icon
Features accessible by menu item
Certain Reports (each of which has a ReportDefinition defining it.)
Certain Uploads (each of which has a FileType defining it.)
Certain BackgroundProcesses (each of which has a ProcessType defining it.)
Each Subscription has a Plan. It's simple enough to tie that Plan into items 3, 4, and 5 above and use cancancan for accessibility by current_user. However, features 1 and 2 are a different story. I can see wrapping their accessibility in a view helper which checks a Feature/Plan list. But that only handles the view. If the user knows the URL, they'd still be able to access the feature by typing the URL in directly. In that case, do I need to handle the authorization again at the controller action level, or is there instead some kind of middleware I could put in to limit accessibility to the features?
Thanks very much.
If it's a simple app, I simply add an admin column on User. If there's more than 2 user types (admin/non-admin/author/editor/etc) then I would make it an Enum field instead of boolean.
Then, inside user.rb I add several methods...
def is_admin?
admin?
end
def is_author?
!admin?
end
From there, I also add some code in application_controller.rb which raise an exception:
def unauthorized
head(:unauthorized)
end
def unprocessable
head(:unprocessable_entity)
end
I also add a current_user method in application_controller.rb:
helper_method :current_user
def current_user
#user ||= User.find(session[:user_id])
end
Then in my views, that's where I handle "hiding" things or disabling buttons.
<%= if current_user.is_admin? %>
<button>Admin button</button>
<% else %>
<button>Author button</button>
<% end %>
This of course is NOT security (view-layer only changes), which is why I also return early from controllers using the previous methods I laid out:
def destroy
return unauthorized unless current_user.is_admin?
# ... delete code here
end
For the example above dont forget to use return or the code will keep executing.
For things that are more than simple, I use Pundit.
I would simple roll my own using enum to set different access or subscription levels. Then just write a before_action called can_access? to block off entire actions. Then I would set some conditionals or view_helpers in the view to block access to certain UI elements.
https://github.com/jnunemaker/flipper is a good solution and does exactly what you are looking for.
Otherwise, like you said, cancancan is good for a naive solution.
I've been researching this topic for a day now, and I haven't seen a solution that could adequately allow this. I would have even give up and said that it's not possible, but I see large companies achieving this in their apps!
I need to know if the current user is following another user. I need to know this many times (for the current_user) without polling the DB again
The solution needs to be friendly for reuse. A solution (that's not friendly for reuse) I had come up with is as follows:
module UsersHelper
def is_following?(user)
return false if current_user == user
user.is_following = Relationship.find_by(followed_id: user.id, follower_id: current_user.id)
end
end
is_following?(#user) can now be called in any controller
Notice that I'm able to access current_user because this helper method will be called in a controller, not a model.
This implementation is cool for one model, maybe two... except I need to do this in almost every many-many relationship I have in the app. So it has to be scalable.
I'm referencing exactly what Twitter does with their following.
UPDATE: All, thanks for the responses - here is some more significant info.
I'm using a Neo4J graph database for the back-end, ROR 3 (MRI), and one single Neo4J database server accessed via REST.
If you don't know much about Neo4j, to use more than one database server (master/master) for data costs $26,000, which means I have to code for optimization now, not later, or come up with $26k...I'm sure you can guess which way I'm going with this..And I'm using it via rest, not locally etc, so performance matters...
I have one database server that has to handle all of the database work, and yes 1 ms counts under this scenario where some other queries take up to 40 ms. So no, I don't want to hit the database unnecessarily as it will simply add unnecessary work to it.
It might be easy to say "don't code for optimizations or problems you don't have yet" yet given the bottleneck and steep costs - and the fact I already have what I need done except for the authentication piece, it really doesn't apply.
What I simply wanted to know, was if the #current_user ||= is valid across pages.. The answer is that it's valid in a request, and not across them or pages. Yes this is a simple question, but sometimes they have to be asked in the midst of R&D and advanced stuff. Hence my gut feeling to stick with sessions to hold the id of the user logged in.
Thanks for your help!
I'm trying to allow a user to login to my site either by cookies or by username and password. The username/password part works fine, but having a problem with introducing cookies....
I've ready plenty of "how tos" including: http://ruby.railstutorial.org/chapters/sign-in-sign-out
I do not want to use a gem, devise, etc.
PROBLEM:
I notice that requests from the page to read current_user (a helper in the application controller) results in database reads, even though I'm using ||=..
I've tried the #current_user ||= User.find by blah blah blah
and it ALWAYS hits the database. This shoudn't be the case right? It should hit once and then that's it, correct?
But before you suggest any tutorial - I've seen tons of them - here is my question.. is #current_user saved across pages, or just for the current page? The link above mentions it only saves it for the current page...
You see, I don't want to keep hitting the database needlessly to find out the same person previously is logged in.. I can do that with session variables.
I really just want to check for a cookie,and would be happy to keep on using session[:user_id] after that.. for performance reasons, I do not want to keep hitting the database.
Any help?
My NEW code is below (and this too always hits the database as it should in this instance). I removed the typical #current_user ||= find_by.. because it was useless - it was always hitting the db.
.. I already tried https://github.com/cliftonm/basic-auth and http://ruby.railstutorial.org/chapters/sign-in-sign-out etc..
(ROR 3.2...)
class ApplicationController < ActionController::Base
protect_from_forgery
#before_filter :set_var
helper_method :current_user
private
def current_user
#current_user = User.find_by_id(session[:user_id]) #if session[:user_id]
if (#current_user)
return #current_user
end
#current_user =User.find_by_remember_token(cookies[:auth_token]) if cookies[:auth_token]
if (#current_user)
return #current_user
end
end
User.find will always hit the database unless you have some kind of cache extension loaded. Rails.cache can be configured several ways, but the most popular, if this sort of thing is required, is Memcached.
Unless you're dealing with massive scaling issues, the time required to fetch a user record should be less than 1ms, so it's hardly worth fussing about. Check your log/development.log to see the execution times of your various queries and focus first on the slowest ones.
Instance variables like #current_user persist only for the duration of the request. Remember that the HTTP protocol is stateless, each request exists independent of the others, and the only way to communicate state is via cookies, a persistent store like a database, a temporary store like an in-memory cache, or by parameters sent in with the request itself via GET or POST.
If you want to save something across pages, add it to the session, but be careful. You should only be persisting things like strings, numbers, or booleans. You should not be adding models. Further, using the default cookie store, each thing you put in the session increases the overhead on all requests made to your application from that point forward until you remove that from your session store.
Don't sweat the little things until you've got all the other problems solved. Business logic first, optimization second.
Neither of these should be slamming the database every time. Please let me know.
if (#current_user)
#current_user
else
#current_user = User.find_by_id(session[:user_id])
end
or
#current_user ||= User.find_by(id: session[:user_id])
Have you tried using a before_filter (instead of a helper) to load current_user only once? You would then store it / access it using an instance variable.
class ApplicationController < ActionController::Base
before_filter :load_current_user
def load_current_user
#current_user = # your code goes here.
end
end
Edit:
My bad. How about storing a server-side encrypted current_user record in the cookie, and keeping the hashsum in the session for later checkup?
I have seen the or equals ||= often used in application controller methods to set a variable if it doesn't exist. The most recent in Railscasts 270. But I have a question.. take for example this helper method
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
From my understand #current_user is set if it doesn't already exist. This means that rails doesn't have to go out to the database, performance win, etc.
However I'm confused as to the scope of #current_user. Lets say we have two users of our website. The first one (lets call him "bob") comes to the site and sets #current_user to be his user object. Now when the second one ("john") comes in and rails asks for #current_user... why isn't the user object still bob's? After all #current_user was already set once when bob hit the site so the variable exists?
Confused.
Variables prefixed with # are instance variables (that is, they are variables specific to a particular instance of a class). John's visit to the site will be handled by a separate controller instance, so #current_user will not be set for him.
I understand that the helpers are not generally accessible in controllers. But why all the wrapping around a instance variable #current_user?
Why does the helper have "current_user=" and "current_user" defined as methods? Why can #current_user be all that is needed for this section?
I think you need to continue reading. From the text:
If we did this, we would effectively replicate the functionality of attr_accessor, first seen in Section 4.4.5 and used to make the virtual password attribute in Section 7.1.1.8 The problem is that it utterly fails to solve our problem: with the code in Listing 9.15, the user’s signin status would be forgotten: as soon as the user went to another page—poof!—the session would end and the user would be automatically signed out.
To avoid this problem, we can find the session user corresponding the cookie...
I have the same question. Yes, I think that current_user is structured this way to prevent code replication. But I also think it might be a syntax issue. If you made current_user a public property of SessionHelper, (I think) you'd access it with SessionHelper.current_user, but if you define a current_user method, and a current_user=(user) method, you can use the current_user property without the extra clutter.
So, I think it's just a style issue. Your approach works fine too.
I did get a good answer from a co-worker.
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
self.current_user = user
end
end
def current_user
#current_user ||= user_from_remember_token
end
It is a refactor issue. You don't want to replicate the code from current user everywhere.
I still think the current_user= method is not needed. I don't see a reason not to use the instance variable for assignment.