I have a Rails 4 app. It is working with devise 3.2.3. devise is properly integrated. At this point, users can register with email and password, sign in and perform CRUD operations.
Now here is what I would like to do: Instead of having any user to sign up by themselves, I want to create an admin. The admin would retain the responsibility of creating users. I don't want users to sign up by themselves. Basically the admin will create the user, issue them their log-in credentials, and email it to them.
I read this post and similar ones in SO and in devise wikis to no avail.
I have added a boolean field to users table to identify admin users.
class AddAdminToUser < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, :default => false
end
end
I have read about managing users using cancan but I don't know how to use it to achieve my objective
The solution i'm looking for would probably require a combination of devise and cancan.
I would appreciate any guidance on this matter.
Make sure that the boolean :admin is not in your params.permit() area for strong parameters.
Use the pundit gem, it is maintained and pretty much plain old ruby objects.
Then in your UserPolicy you would do something like this
class UserPolicy < ApplicationPolicy
def create?
user.admin?
end
end
And your model would look something like this
class User < ActiveRecord::Base
def admin?
admin
end
end
Last in your controller you make sure that the user is authorized to do the action
class UserController < ApplicationController
def create
#user = User.new(user_params)
authorize #user
end
end
You would probably also want to restrict the buttons that are shown that would give access to the admin user creation section. Those can be done with pundit as well.
Related
Feel free to say if you think something is wrong.
I extended Devise Registration controller to create a Profile object to every new user:
class Users::RegistrationsController < Devise::RegistrationsController
def new
resource = build_resource({})
resource.profile = Profile.new
resource.profile.user_id = #user.id
respond_with resource
end
They both are has_one - has_one related and in database:
create_table :profiles do |t|
t.belongs_to :user, index: { unique: true }, foreign_key: true
end
So to get the right profile of current user, I must:
private
def set_profile
#profile = Profile.where(user_id: current_user.id).first
end
And this kinda solves the problem - seems other users cant go around this query and access other profiles (or CAN THEY?), but for other resources I use Pundit to control authorisation, so now it feels a bit messy.
So thats one concern. Other - I still don't know how to act when there is no user logged, because if visiting any restricted resource, this:
private
def set_some_resource
end
end
Throws - "undefined method `id' for nil:NilClass) - how is best to avoid this?
Thanks for any advices.
You may want to start by reading the Rails guides on assocations.
To create a one to one association you use belongs_to on the side with the foreign key column and has_one on the other.
class User
has_one :profile
end
class Profile
belongs_to :user
end
ActiveRecord then automatically links the records together. In general you should avoid setting ids (or getting associated records by ids) explicitly and instead use the assocations:
class Users::RegistrationsController < Devise::RegistrationsController
# ...
def new
# calls Devise::RegistrationsController#new
super do |user|
user.profile.new
end
end
end
Devise is pretty nifty and lets you pass a block to tap into the flow instead of copypasting the whole action.
Simularily you would fetch the current users profile with:
private
def set_profile
#profile = current_user.profile
end
You can set if the callback should be called by using the if: option.
before_action :set_profile, if: :user_signed_in?
But if the action requires authentication you should make sure that it is after :authenticate_user! anyways which will halt the filter chain.
And this kinda solves the problem - seems other users cant go around
this query and access other profiles (or CAN THEY?), but for other
resources I use Pundit to control authorisation, so now it feels a bit
messy.
You don't need to use Pundit to authorize creating a profile or fetching the current users profile. Since the profile is fetched via the user the is no way for another user to access it (well without hacking).
what you might want to authorize is the show, index, edit etc actions if you create a ProfilesController.
I am trying to make an app with Rails 4 and devise and pundit (with rolify for roles)
In the:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
According to this Go Rails video: https://gorails.com/episodes/authorization-with-pundit?autoplay=1
This attr_reader section in the application policy covers all of the rest of the policies - so you don't need to repeat in each one.
However, my current question is given that I use devise, should I change the application policy to use current_user instead of user? Eg:
class ApplicationPolicy
attr_reader :current_user, :record
def initialize(current_user, record)
#current_user = current_user
#record = record
end
None of the examples I've found do it this way, but I don't understand why not.
I was hoping to figure out if I'm off the the right start before I start writing rules for each policy. Does every controller action that i make a policy for need to refer to user or should I change all of them to current_user?
You can use whatever you want to do the authorization logic.
Pundit will send in current_user from your controller when you call something like authorize #object.
From in your class you will just have to do your logic with current_user instead of user.
Why would you want to change it? From your applications perspective, you are really authorizing a user to do something, not necessarily a current_user. So keeping it as user follows conventions
while doing admin work, i'd like to disable user logins --
is there some way to use devise for this -- I don't THINK this
is suitable for rolify -- because this is a temporary disablement --
thanks in advance for any help,
rick
Back-End
If you wanted to create a "maintenance" mode, you'll be best doing something like this:
#app/models/user.rb
class User < ActiveRecord::Base
end
#app/models/admin.rb
class Admin < User
def maintainance!
self.toggle! :maintainance
end
end
This will need a maintenance column in the users table, and you'll have to add a type column in the users table, too.
You could get away with keeping this in the User model, however, you'd need some conditions to determine whether the user is an admin. Since you didn't specify how you're differentiating, above is how we do it.
--
You'd be able to call it like this:
#app/controllers/users_controller.rb
class SettingsController < ApplicationController
before_action :authenticate_user!
def maintenance
current_user.maintenance! #-> toggles so you'll just be able to call this as you need.
end
end
#config/routes.rb
resources :settings, only: [] do
put :maintenance #-> url.com/settings/maintenance (considering current_user present)
end
This will allow you to set the "maintenance" mode through your user settings area. If you don't have one, you'll be able to use the above code to get it working.
Front-End
With the backend in place, you'll be able to then manage the front-end.
To do this, you'll need a helper to determine if any user has set the "maintenance" mode...
#app/helpers/application_helper.rb
class ApplicationHelper
def maintenance_mode?
Admin.exists? maintenance: true
end
end
This will allow you to use this helper to determine whether you should allow Devise to accept logins or not:
#app/views/devise/sessions/new.html.erb
<% unless maintenance_mode? %>
... devise form ...
<% end %>
The helper will execute a DB request, but keeping it in the devise areas only (IE it's not "site wide") should make it okay.
#app/controllers/devise/sessions_controller.rb
class SessionsController < Devise::SessionsController
before_action :check_maintenance
private
def check_maintenance
redirect_to root_path, notice: "Sorry, maintenance mode is in effect; no logins." if maintenance_mode?
end
end
This will prevent any controller-based actions from firing.
Finally, if you want to get rid of any logged-in users, you'll need to do something quirky, like resetting the sessions or something similar:
How can I reset all devise sessions so every user has to login again?
Devise force signout
Here's what I'd do:
1. Create a method for your User model. It could be something like active, or able_to_login.
2. Set this attribute to :boolean.
3. Use rails console. Use the console to set the active method to true or false, enabling or disabling your users to access your application:
user = User.all
user.each do |u|
u.active = false # or
u.able_to_login = false
u.save
end
I don't think this is the best method, but it should work without installing another gem or heavy code.
In your /models/user.rb add this method
def active_for_authentication?
super && is_admin?
end
def is_admin?
# returns true if user is admin
end
This is the "Devise way" of doing this :)
My rails app has a few cab operators and they have a few cabs associated with them, and they are related as follows:
class Operator < ActiveRecord::Base
has_many :cabs
end
I have used Devise as my authentication gem. It authenticates users, admins and super admins in my app. I have created separate models for users, admins and super admins (and have not assigned roles to users per se).
I now wish to add the authorization feature to the app, so that an admin (who essentially would be the cab operator in my case) can CRUD only its own cabs. For e.g., an admins belonging to operator# 2 can access only the link: http://localhost:3000/operators/2/cabs and not the link: http://localhost:3000/operators/3/cabs.
My admin model already has an operator_id that associates it to an operator when an admin signs_up. I tried to add the authorization feature through CanCan, but I am unable to configure CanCan to provide restriction such as the one exemplified above.
I also tried to extend my authentication feature in the cabs_controller, as follows:
class CabsController < ApplicationController
before_action :authenticate_admin!
def index
if current_admin.operator_id != params[:operator_id]
redirect_to new_admin_session_path, notice: "Unauthorized access!"
else
#operator = Operator.find(params[:operator_id])
#cabs = Operator.find(params[:operator_id]).cabs
end
end
But this redirects me to the root_path even if the operator_id of the current_admin is equal to the params[:operator_id]. How should I proceed?
EDIT:
Following is my routes.rb file:
Rails.application.routes.draw do
devise_for :super_admins
devise_for :users
resources :operators do
resources :cabs
end
scope "operators/:operator_id" do
devise_for :admins
end
end
I have three tables: users, admins and super_admins. I created these coz I wanted my admins to hold operator_ids so that the admins corresponding to an operator can be identified. Also, I wanted the admin sign_in paths to be of the type /operators/:operator_id/admins/sign_in, hence the tweak in the routes file.
Unfortunately, initially I didn't understand that you actually have 3 different tables for users and (super)admins... Not sure that Pundit can help you in this case, but I'll keep the old answer for future visitors.
Coming back to your problem, let's try to fix just the unexpected redirect.
Routes seems fine, so the problem can be one of this:
You're getting redirected because you're currently not logged in as an admin, so you don't pass the :authenticate_admin! before_action.
You say "even if the operator_id of the current_admin is equal to the params[:operator_id]", but this condition is probably not true. Can you debug or print somewhere the value of both current_admin.operator_id and params[:operator_id] to see if they're actually equals?
Another interesting thing, is that you have a redirect for new_admin_session_path in your code, but then you say "this redirects me to the root_path". Can you please double check this?
OLD ANSWER
If you want to setup a good authorization-logic layer, I advice you to use pundit.
You've probably heard about cancan, but it's not supported anymore...
Leave Devise managing only the authentication part and give it a try ;)
PUNDIT EXAMPLE
First of all, follow pundit installation steps to create the app/policies folder and the base ApplicationPolicy class.
Then, in your case, you'll need to create a CabPolicy class in that folder:
class CabPolicy < ApplicationPolicy
def update?
user.is_super_admin? or user.cabs.include?(record)
end
end
This is an example for the update action. The update? function have to return true if the user has the authorisation to update the cab (You'll see later WHICH cab), false otherwise. So, what I'm saying here is "if the user is a super_admin (is_super_admin? is a placeholder function, use your own) is enough to return true, otherwise check if the record (which is the cab your checking) is included in the cabs association of your user".
You could also use record.operator_id == record.id, but I'm not sure the association for cab is belongs_to :operator. Keep in mind that in CabPolicy, record is a Cab object, and user is the devise current_user, so implement the check that you prefer.
Next, in your controller, you just need to add a line in your update function:
def update
#cab = Cab.find(params[:id]) # this will change based on your implementation
authorize #cab # this will call CabPolicy#update? passing current_user and #cab as user and record
#cab.update(cab_params)
end
If you want to make things even better, I recommend you to use a before_action
class CabsController < ApplicationController
before_action :set_cab, only: [:show, :update, :delete]
def update
#cab.update(cab_params)
end
#def delete and show...
private
def set_cab
#cab = Cab.find(params[:id])
authorize #cab
end
And of course, remember to define also show? and delete? methods in your CabPolicy.
A have a bunch of controllers with the Admin namespace. I want to restrict access to these unless the user is an admin. Is there a way to do this using CanCan without having to call unauthorized! in every method of every controller?
Add an application controller to your namespace and a before filter to it.
class ApplicationController < ActionController::Base
end
class Admin::ApplicationController < ApplicationController
# these goes in your namespace admin folder
before_filter :check_authorized
def check_authorized
redirect_to root_path unless can? :admin, :all
end
end
class SomeadminController < Admin::ApplicationController
def some_action
# do_stuff
end
end
The Admin Namespaces wiki page for CanCan lists out several solutions to this problem.
As #mark suggested, have a base controller for admins which checks authorization for every action.
You may not need to use CanCan at all for this if all you require is to check that users have an admin
flag.
For handling admins differently from each other (as opposed to differently from regular users only),
consider a separate AdminAbility class (this is a little off-topic, but could prove relevant).
now rails_admin has full support with Cancan, you can find it in its official website, there is a wiki page for this topic:
Rails Admin's authorization with CanCan: