I'm using Cancancan for authorization in ActiveAdmin.
Everything work fine except the :create. When create a new admin, cancancan will check is admin_user.id = id. However, ActiveAdmin make id = nil, so I can't create a new admin.
include CanCan::Ability
def initialize(admin_user)
can :manage, AdminUser, id: admin_user.id
....
end
end
My solution is everyone can skip authorization for create.
My application controller:
class ApplicationController < ActionController::Base
load_and_authorize_resource
skip_authorize_resource :only => :new
end
but it does nothing. Please help!
You can specify what actions need authorization like this...
def initialize(admin_user)
can [:edit, :destroy], AdminUser, id: admin_user.id
end
Just replace/add to [:edit, :destroy] with whatever actions you need that authorization for.
If you want all admin users to be able to perform an action on AdminUser...
can :some_action, AdminUser
Related
I'm stuck with a very simple Rails problem.
I have a blog controller which has these lines:
skip_before_action :check_admin, only: [:index, :show], raise: false
...
private
...
def check_admin
redirect_to(root_path) unless current_user && current_user.admin?
end
I want the behavior to be the following:
if a user is not logged in or is logged in and its role is not admin, and tries to access other actions rather than :index and :show, it should redirect to the root path.
But right now, it shows the user login page for every action when a user is not logged in, whereas it allows every action for any logged in user, even not admin ones.
I am using Devise and CanCanCan. My ability.rb looks like this:
def initialize(user)
can :read, :all # allow everyone to read everything
if user && user.admin?
can :access, :rails_admin # only allow admin users to access Rails Admin
can :dashboard # allow access to dashboard
can :manage, :all
end
end
Where am I failing?
EDIT: I changed the action call in the controller to this:
before_action :check_admin, except: [:index, :show]
Now, the desired behavior is showing for logged users, whereas for unlogged users it is still redirecting to the user login page for every action including index and show.
EDIT 2: I have before_action :authenticate_user! in application_controller.rb while some other controllers are set with skip_before_action :authenticate_user!, :only => [:index]. But this shouldn't interfer with the blog controller in my opinion...
Also, I have the following in config/initializers/rails_admin.rb:
RailsAdmin.config do |config|
### Popular gems integration
## == Devise ==
config.authenticate_with do
warden.authenticate! scope: :user
end
config.current_user_method(&:current_user)
The problem is you have before_action :authenticate_user! in your application controller. if you see your controller definition,:
class BlogController< ApplicationController
'<' operator means BlogController inherits from ApplicationController, therefore all your controllers (except those with skip_before_action) will require authentication.
you'll need
Class BlogController < ApplicationController
skip_before_action :authenticate_user!, :only => [:index, :show]
Currently, my Users database has a column called "admin" with a boolean value and the default set to false. I have one admin user seeded into the database.
How do write my application so that users who are the admin can create new users, but users who are not cannot? (Also, users should be created only by the admin)
It seems like there should be a simple way to do this in devise that does not involve using some external module. So far however, I have not been able to find a satisfactory answer.
I would be more likely to mark the solution which is devise only. (One that is simply standard MVC/Rails solution a plus) However, if there really is a better way to do it that doesn't involve CanCan I may accept that too.
NOTE:
I have been searching around for a while and I've found several other stackoverflow questions that are very similar to this one, but either don't quite answer the question, or use other non-devise modules. (Or both)
To implement the authorization, use a method on the controller
Exactly as suggested by #diego.greyrobot
class UsersController < ApplicationController
before_filter :authorize_admin, only: :create
def create
# admins only
end
private
# This should probably be abstracted to ApplicationController
# as shown by diego.greyrobot
def authorize_admin
return unless !current_user.admin?
redirect_to root_path, alert: 'Admins only!'
end
end
To sidestep the Devise 'already logged in' problem, define a new route for creating users.
We will simply define a new route to handle the creation of users and then point the form to that location. This way, the form submission does not pass through the devise controller so you can feel free to use it anywhere you want in the normal Rails way.
# routes.rb
Rails.application.routes.draw do
devise_for :users
resources :users, except: :create
# Name it however you want
post 'create_user' => 'users#create', as: :create_user
end
# users/new.html.erb
# notice the url argument
<%= form_for User.new, url: create_user_path do |f| %>
# The form content
<% end %>
This seems like the simplist approach. It just requires sub-classing the devise controller. See the docs for how to do that.
# app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
before_action :authenticate_user!, :redirect_unless_admin, only: [:new, :create]
skip_before_action :require_no_authentication
private
def redirect_unless_admin
unless current_user.try(:admin?)
flash[:error] = "Only admins can do that"
redirect_to root_path
end
end
def sign_up(resource_name, resource)
true
end
end
# config/routes.rb
Rails.application.routes.draw do
devise_for :users, :controllers => { :registrations => 'registrations'}
end
Explaination:
Subclass the registration controller and create the route for it.
The before_action ensures that a user is logged in, and redirects them unless they are admin, if they try to sign up.
The already logged in issue is caused by Devise's require_no_authentication method and skipping it resolves the issue.
Next is that the newly created user is automatically signed in. The sign_up helper method which does this is overridden to do prevent automatic signup.
Finally, the Welcome! You have signed up successfully. sign up flash message can be changed via editing the config/locales/devise.en.yml file, if desired.
The problem is conceptual. Devise is only an Authentication library not an Authorization library. You have to implement this separately or use CanCan. Fret not however, it is easy in your case to implement this since you only have one role.
Guard your user create/update/destroy action with a before filter:
class UsersController < ApplicationController
before_filter :authorize_admin, except [:index, :show]
def create
# user create code (can't get here if not admin)
end
end
class ApplicationController < ActionController::Base
def authorize_admin
redirect_to root_path, alert: 'Access Denied' unless current_user.admin?
end
end
With this simple approach you run a before filter on any controller action that can affect a user record by first checking if the user is an admin and kicking them out to the home page if they're not.
I had this issue when working on a Rails 6 application.
I had an admin model which I Devise used to create admins.
Here's how I fixed it:
First I generated a Devise controller for the Admin model, which created the following files:
app/controllers/admins/confirmations_controller.rb
app/controllers/admins/omniauth_callbacks_controller.rb
app/controllers/admins/passwords_controller.rb
app/controllers/admins/registrations_controller.rb
app/controllers/admins/sessions_controller.rb
app/controllers/admins/unlocks_controller.rb
Next, I modified the new and create actions of the app/controllers/admins/registrations_controller.rb this way to restrict signup to admin using Devise:
class Admins::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
if admin_signed_in?
super
else
redirect_to root_path
end
end
# POST /resource
def create
if admin_signed_in?
super
else
redirect_to root_path
end
end
.
.
.
end
Afterwhich, I added the skip_before_action :require_no_authentication, only: [:new, :create] action to the app/controllers/admins/registrations_controller.rb this way to allow an already existing admin to create a new admin while being logged in:
class Admins::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
skip_before_action :require_no_authentication, only: [:new, :create]
.
.
.
end
You may need to also hide the Register and Forgot password buttons in the login form, to only been by logged in admins:
# app/views/admins/sessions/new.html.erb
<% if admin_signed_in? %>
<%= render "admins/shared/links" %>
<% end %>
Optionally, I added first_name and last_name parameters to the allowed parameters in the app/controllers/admins/registrations_controller.rb file, after already adding it to the admin model through a database migration:
class Admins::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
skip_before_action :require_no_authentication, only: [:new, :create]
.
.
.
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name,
:last_name])
end
# If you have extra params to permit, append them to the sanitizer.
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name,
:last_name])
end
.
.
.
end
Note: You will need to modify your app/views/admin/registrations views accordingly as well to cater for this
Additionally, you may need to create a new controller where you can create the index action for admins, say, app/controllers/admins/admins_controller.rb:
class Admins::AdminsController < ApplicationController
before_action :set_admin, only: [:show, :edit, :update, :destroy]
before_action :authenticate_admin!
# GET /admins
# GET /admins.json
def index
#admins = Admin.all
end
.
.
.
end
Note: If you need more control like managing admins by updating and deleting information of other admins from your dashboard, you may need to add other action like show, edit, update, destroy, and others to this file.
And for the routes:
## config/routes.rb
# List of Admins
get 'admins', to: 'admins/admins#index'
namespace :admins do
resources :admins
end
And then modify the path used after sign up in the app/controllers/admins/registrations_controller.rb to redirect to the admins_path, this will show you a list of all the admins that have been created:
class Admins::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
skip_before_action :require_no_authentication, only: [:new, :create]
.
.
.
# The path used after sign up.
def after_sign_up_path_for(resource)
admins_path
end
# The path used after sign up for inactive accounts.
def after_inactive_sign_up_path_for(resource)
admins_path
end
end
Finally, you may need to change the notice that Devise gives when you create a user in the config/locales/devise.en.yml file, from:
Welcome, You have signed up successfully
to, say:
Account was created successfully.
That's all.
I hope this helps.
I have a nested resource for which I'm using Cancan to do authorization. I need to be able to access the parent object in order to be able to authorize the :index action of the child (since no child instance is passed for an :index action).
# memberships_controller.rb
class MembershipsController < ApplicationController
...
load_and_authorize_resource :org
load_and_authorize_resource :membership, through: :org
..
end
ability.rb
can [:read, :write], Membership do |membership|
membership.org.has_member? user
end
This doesn't work for the :index action
Unfortunately the index action doesn't have any membership instance associated with it and so you can't work your way back up to check permissions.
In order to check the permissions, I need to interrogate the parent object (the org) and ask it whether the current user is a member e.g.
# ability.rb
...
can :index, Membership, org: { self.has_member? user }
Cancan almost lets me do this...
Cancan states that you can access the parent's attributes using the following mechanism:
https://github.com/ryanb/cancan/wiki/Nested-Resources#wiki-accessing-parent-in-ability
# in Ability
can :manage, Task, :project => { :user_id => user.id }
However this just works by comparing attributes which doesn't work for my case.
How can I access the parent object itself though?
Is there any way to access the parent object itself within the permissions?
I recently faced the same problem and ended up with the following (assuming you have Org model):
class MembershipsController < ApplicationController
before_action :set_org, only: [:index, :new, :create] # if shallow nesting is enabled (see link at the bottom)
before_action :authorize_org, only: :index
load_and_authorize_resource except: :index
# GET orgs/1/memberships
def index
#memberships = #org.memberships
end
# ...
private
def set_org
#org = Org.find(params[:org_id])
end
def authorize_org
authorize! :access_memberships, #org
end
end
ability.rb:
can :access_memberships, Org do |org|
org.has_member? user
end
Useful links
https://github.com/ryanb/cancan/issues/301
http://guides.rubyonrails.org/routing.html#shallow-nesting
Can't you do something like this?
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :index, Membership, org: {id: user.memberships.map(&:org_id)}
end
end
Good Afternoon All,
I am trying to sort out my user authentication and causing myself headaches.
I have a :role_type defined in User and my user has two roles, Employer or Developer, now I my user is currently developer and should be able to see jobs#index but it cannot and I get the default cancan message of unauthorized:
class JobsController < ApplicationController
load_and_authorize_resource
before_filter :authenticate_user!
before_action :set_job, only: [:show, :edit, :update, :destroy]
and here is the Ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :create, :delete, :update, Job if user.role_type == "Employer"
can :read, Job if user.role_type == "Developer"
end
end
Thanks for the help.
As written in the comments: Make sure that you are not having a typo like "Employer" instead of "employer" :-)
I'm assuming the error occurs when the user.role_type == 'Employer'
Looking at your code it looks like the Employer is missing the :read action.
If Employer can manage all jobs (basic crud actions) you might want to consider giving him the :manage ability
Example:
can :manage, Job if user.role_type == "Employer"
can :read, Job if user.role_type == "Developer"
The :manage ability gives the user access to every action within the JobsController. See the following doc for clarification on the :manage ability: https://github.com/ryanb/cancan/wiki/defining-abilities#the-can-method
Otherwise you would need to add the :read action to Employer, as they are not a Developer and will not get the :read action
Following the guide here, I added a boolean attribute to my database using a migration:
rails generate migration add_admin_to_user admin:boolean
I've configured my account to be an admin (admin = 1) via Rails console. I have a controller that I want to restrict access to certain actions (new, edit, create, and destroy) for administrators only.
I'll also have normal users, I just want to restrict access to these actions for admins only in this controller. Currently, I'm using the code:
before_filter :authenticate_user!, :only => [:new, :edit, :create, :destroy]
Which restricts access to registered users -- how do I take this a step further and require admins?
you can easily implement your own before_filter to allow access to only admin users by using the .admin? method associated with your user model. for instance:
before_filter :verify_is_admin
private
def verify_is_admin
(current_user.nil?) ? redirect_to(root_path) : (redirect_to(root_path) unless current_user.admin?)
end
You will want to define your own method in the before filter and then detect whether the user is an admin or not in that method prior to calling :authenticate_user!
before_filter :custom_method, :only => [:new, :edit, :create, :destroy]
private
def custom_method
authenticate_user!
if current_user.admin
return
else
redirect_to root_url # or whatever
end
end
You will want to do the authenticate_user! step prior to checking the current_user variable.
ian.