Cancan - new form fails when user restricted to certain sites - ruby-on-rails

I have users
class User < ActiveRecord::Base
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :email, :password, :password_confirmation,
:remember_me, :site_id, :role_name
belongs_to :site
end
sites
class Site < ActiveRecord::Base
has_many :users
has_one :front_page_campaign
end
and front_page_campaigns
class FrontPageCampaign < ActiveRecord::Base
belongs_to :site
end
I'm using cancan to restrict access, so users can only manage front_page_campaigns for their own site:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
case user.role_name
when "super_admin"
# can do everything
can :manage, :all
when "editor"
# can edit content for their site
can [:create, :read, :update], FrontPageCampaign, site_id: user.site_id
end
end
end
This works perfectly for users with role_name super_admin and also for editor on show and edit on front_page_campaigns. But when an editor tries to create a new front_page_campaign, I get a cancan forbidden notice
You are not authorized to access this page.
The standard form offers a dropdown box of all sites, and I guess I need to restrict this to just the user's own site. How would I go about doing this?

Your authorization issue is solved by adding:
can :new, FrontPageCampaign
to the editor section of cancan ability init.
To set site_id on the new and create object you can set up a before_filter:
# FrontPageCampaignsController
before_filter :set_site_id, :only => [:new, :create]
protected
def set_site_id
#resource.site_id = current_user.site_id if current_user && current_user.role_name == 'editor'
end
you gotta make sure this fires after the resource is created but before can can authorization.
In your form (if you use the same for superadmin and editor) make the site dropdown selection readonly or hidden if current_user.role_name == 'editor'.
Note that if someone tampers with the form and sends an alien site_id as editor, it will be corrected by the before filter, which is not nice. If you take if out and have :only => :new then they will get authorization error by cancan. If you are super pedant, you should instead get a valid response with validation error. You can achieve this by 1) applying the before_filter only to new and 2) say in ability init
when "editor"
# can edit content for their site
can [:read, :update], FrontPageCampaign, site_id: user.site_id
can [:create, :new], FrontPageCampaign
end
and 3) add site owner checking to model validation. This is my preferred way, keeping authorization errors for illegal access of existing resources.
hope this answers your question

Related

User redirection based on role in ruby on rails

My requirement is, i have to be able to register as a Super Admin/ admin / Guest and after that, i have to create a sample project as a Super Admin/ admin / Guest.
Super admin have all the permissions like Create,view, edit, delete. Admin have permissions like edit, view and Guest has to have only create access.
But, in my application, Guest is also getting all the permissions like create, update, delete on projects list.
To resolve this problem,
The gems which i used are:
gem 'devise'
gem 'cancancan', '~> 1.10'
gem 'rolify'
for redirection based on the role is in devise / registration / new.html.erb:
once, i create a new user,(by selecting radio buttons) i was able to save its value 1 (for super admin) / 2 (Admin) / 3(Guest) into the database
app/models/ability.rb:
class Ability
include cancan::Ability
def initialize(user)
user ||= User.new
if user.has_role?(:super_admin)
can :manage, :all
elsif user.has_role?(:admin)
can :create, Project
can :update, Project do |project|
project.ongoing?
end
can :read, Project
elsif user.role?(:guest)
can :create, Project
end
end
end
app/models/role.rb:
class Role < ActiveRecord::Base
resourcify
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource,
:polymorphic => true,
validates :resource_type,
:inclusion => { :in => Rolify.resource_types },
:allow_nil => true
scopify
end
app/models/users.rb:
class User < ActiveRecord::Base
rolify :role_cname => 'Usertype'
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
has_many :projects, dependent: :destroy
def super_admin?
has_role?(:super_admin)
end
def admin?
has_role?(:admin)
end
def guest?
has_role?(:guest)
end
end
Well for starters in your last statement you are calling:
elsif user.role?(:guest)
whereas in the others you are calling
elsif user.has_role?
Any questions ask below. Since I am not able to comment yet.

Using after_action to create account after a Devise registration for rails

Done a lot of reading on the situation below. Want some opinions.
In the thread at (1) the user asks "How to create another object when creating a Devise User from their registration form in Rails?". This is exactly what I am trying to do as well. When a user registers I want to create an Account object.
(1) How to create another object when creating a Devise User from their registration form in Rails?
However there's a problem I've been having, which also surfaced at the referenced thread. When I run the following in my subclassed registrations controller. I get "When assigning attributes, you must pass a hash as an argument." ArgumentError in RegistrationsController#create.
class RegistrationsController < Devise::RegistrationsController
after_action :create_account, only: [:create, :update]
private
def create_account
Account.create(#user)
end
end
It turns out that using callbacks don't work because I don't have access to the params hash. I've tried so many different variations of the Account.create(current_user) or (#user) as well as adding another def below to try to permit the user id param to go to user_id foreign key of Account. Still the same error "When assigning attributes, you must pass a hash as an argument."
def account_params
params.require(:account).permit(:user_id,:user)
end
This is where the question at (1) ended up. It proposed bypassing Devise's RegistrationsController.
Is this the only remaining way to achieve the objective of creating other models when a User registers in Devise? Any other suggestions? I don't want to commit to bypassing the registrations controller unless its the only way.
Thanks for any suggestions and happy to provide any other information.
class Account < ApplicationRecord
has_many :users
end
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :projects
has_many :notes
belongs_to :account
In your user.rb file, you could do this:
after_create :create_account
def create_account
#user = User.last
#account = Account.new
#user.account = #account
#user.save
#account.save
end

How to add an admin restriction feature through Devise to my rails app?

My rails app has a few cab operators and they have a few cabs, and they are related as follows:
class Operator < ActiveRecord::Base
has_many :cabs
end
I wish to add authentication system so as to create admins for each operator. I am using Devise. Since I need to create path as: operator/:operator_id/admins/sign_up, I generated the Admin model, as:
rails generate devise Admin
Then I modified my routes so as to obtain the above mentioned path:
scope "operators/:operator_id" do
devise_for :admins
end
After a few more modifications, I was able to add new admins and associate them to appropriate operators. However, I want to ensure that an admin only has access to the cabs of the operator to which the admin is associated. Adding the following to the cabs_controller doesn't work:
before_action :authenticate_admin!
as a signed_in admin has access to all other operator's cabs. I want to make sure that:
1. If there is no current_admin, my app asks to sign_in or sign_up
2. If there already is a current__admin signed_in, he/she has access to only the cabs associated to that operator to which the current_admin is assigned.
I am new to Devise. Please advise how I should proceed. Thanks!
Devise is for Authentication, your problem is an Authorization problem which Devise cannot help. You can look into Cancan https://github.com/ryanb/cancan :)
You don't need to scope your devise for admins. I'm not sure if it would solve any issue with access. It also makes your code harder to maintain.
I would advise you to focus on relationships instead
class Operator < ActiveRecord::Base
has_many :cabs
has_many :admins
end
class Operator < ActiveRecord::Base
has_many :cabs, through: operator
belongs_to :operator
end
Normally the way I handle such cases later is to write special in the cab model :
def is_admin?(admin)
admin.cabs.include?(#cab)
# you can put more logic here if you have different levels of access
end
and later in the controller when you want to restrict access:
if #cab.is_admin?(current_admin)
# do the stuff
else
# go away message
end
or for instance if you list cabs in index:
def index
current_admin.cabs.page(params[:page]) # this is assuming you're using pagination with something like Kaminari
end
current_admin from divise, you may want to also run :authenticate_admin! before filter to make sure you have you admin and it's not nil
Of course you can put it in price message and run it as before filter.
The most simple solution is to use STI and have Admin < User, then a couple of
devise_for :users
devise_for :admins
If you do not want to use STI, some custom overridings may do the trick, if you add a role method to your users table for exemple.
#routes
devise_for :users
namespace :admin do
devise_for :admins,
singular: :admin,
plural: :admins,
class_name: 'User',
only: [:sessions],
path: '/',
module: :devise
end
#lib/overrides/devise
require "devise/strategies/authenticatable"
require "devise/strategies/database_authenticatable"
module Devise
module Strategies
class DatabaseAuthenticatable
def authentication_hash
if mapping.name == :admin
#authentication_hash.update(role: :admin) #will add a where(role: :admin) clause
else
#authentication_hash
end
end
end
end
end
class Admin::ApplicationController < ApplicationController
skip_before_filter :authenticate_user!
before_filter :authenticate_admin!
end
You can use role base Authentication.
Generate User model
class User < ActiveRecord::Base
enum role: [:operator, :admin]
after_initialize :set_default_role, :if => :new_record?
def set_default_role
self.role ||= :operator
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
generate migration to add role
rails g AddRoleToUser role:integer
Please take a look ENUM

scoping with active admin cancan

How can i use scoping with active admin & cancan.
I have admin users & those have (has_one) relation with institution
and institution has many profiles
Now when admin user login then i want display all profiles which has same institution.
Doesn't find following link much helpful.
http://activeadmin.info/docs/2-resource-customization.html#scoping_the_queries
if you just do simply this, do you get a problem?
# ability.db
def initialize(user)
case
# ...
when user.super_admin?
can :manage, :all
when user.admin?
can :manage, Profile, :institution_id => user.institution.id
#
# ...
end
this will allow: Profile.accessible_by(current_user), which here is same as current_user.profiles
class AdminUser
has_one :institution
has_many :profiles, :through => :institution
end
ActiveAdmin.register Profile do
scope_to :current_user #here comes the variable which set in initializer
end
if you want superadmin to access all posts, you can use the :association_method option
ActiveAdmin.register Profile do
scope_to :current_user, :association_method => :admin_profiles
end
# in class User
def admin_profiles
if super_admin?
Profile.unscoped
else
profiles
end
end
A tricky solution could generalize this and use a delegator class as proxy to unscope all models for superadmins. i can spell out on request.

Rails: Using Devise with single table inheritance

I am having a problem getting Devise to work the way I'd like with single table inheritance.
I have two different types of account organised as follows:
class Account < ActiveRecord::Base
devise :database_authenticatable, :registerable
end
class User < Account
end
class Company < Account
end
I have the following routes:
devise_for :account, :user, :company
Users register at /user/sign_up and companies register at /company/sign_up. All users log in using a single form at /account/sign_in (Account is the parent class).
However, logging in via this form only seems to authenticate them for the Account scope. Subsequent requests to actions such as /user/edit or /company/edit direct the user to the login screen for the corresponding scope.
How can I get Devise to recognise the account 'type' and authenticate them for the relevant scope?
Any suggestions much appreciated.
There is an easy way to handle STI in the routes.
Let's say you have the following STI models:
def Account < ActiveRecord::Base
# put the devise stuff here
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
def User < Account
end
def Company < Account
A method that is often overlooked is that you can specify a block in the authenticated method in your routes.rb file:
## config/routes.rb
devise_for :accounts, :skip => :registrations
devise_for :users, :companies, :skip => :sessions
# routes for all users
authenticated :account do
end
# routes only for users
authenticated :user, lambda {|u| u.type == "User"} do
end
# routes only for companies
authenticated :user, lambda {|u| u.type == "Company"} do
end
To get the various helper methods like "current_user" and "authenticate_user!" ("current_account" and "authenticate_account!" are already defined) without having to define a separate method for each (which quickly becomes unmaintainable as more user types are added), you can define dynamic helper methods in your ApplicationController:
## controllers/application_controller.rb
def ApplicationController < ActionController::Base
%w(User Company).each do |k|
define_method "current_#{k.underscore}" do
current_account if current_account.is_a?(k.constantize)
end
define_method "authenticate_#{k.underscore}!" do
|opts={}| send("current_#{k.underscore}") || not_authorized
end
end
end
This is how I solved the rails devise STI problem.
I just ran into the exact scenario (with class names changed) as outlined in the question. Here's my solution (Devise 2.2.3, Rails 3.2.13):
in config/routes.rb:
devise_for :accounts, :controllers => { :sessions => 'sessions' }, :skip => :registrations
devise_for :users, :companies, :skip => :sessions
in app/controllers/sessions_controller.rb:
class SessionsController < Devise::SessionsController
def create
rtn = super
sign_in(resource.type.underscore, resource.type.constantize.send(:find, resource.id)) unless resource.type.nil?
rtn
end
end
Note: since your Accounts class will still be :registerable the default links in views/devise/shared/_links.erb will try to be emitted, but new_registration_path(Accounts) won't work (we :skip it in the route drawing) and cause an error. You'll have to generate the devise views and manually remove it.
Hat-tip to https://groups.google.com/forum/?fromgroups=#!topic/plataformatec-devise/s4Gg3BjhG0E for pointing me in the right direction.
try to change routes like so:
devise_for :accounts, :users, :companies
because Devise uses plural names for it's resources
Please let me know if it help you
I don't think this is possible without overriding the sessions controller. Each sign_in page has a specific scope that devise will authenticate against as defined by your routes.
It may be possible to use the same sign_in page for multiple user scopes by using the devise_scope function in your routes file to force both :users and :companies to use the same sign in page (a how-to can be found here), but I'm pretty certain that you would have to modify your sessions controller to do some custom logic in order to determine which type of user is signing in.

Resources