scoping with active admin cancan - ruby-on-rails

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.

Related

Rails 4: CanCanCan abilities with has_many :through association

I have a Rails app with the following models:
class User < ActiveRecord::Base
has_many :administrations
has_many :calendars, through: :administrations
end
class Calendar < ActiveRecord::Base
has_many :administrations
has_many :users, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
For a given calendar, a user has a role, which is define in the administration join model.
For each calendar, a user can have only one of the following three roles: Owner, Editor or Viewer.
These roles are currently not stored in dictionary or a constant, and are only assigned to an administration as strings ("Ower", "Editor", "Viewer") through different methods.
Authentication on the User model is handled through Devise, and the current_user method is working.
In order to only allow logged-in users to access in-app resources, I have already add the before_action :authenticate_user! method in the calendars and administrations controllers.
Now, I need to implement a role-based authorization system, so I just installed the CanCanCan gem.
Here is what I want to achieve:
All (logged-in) users can create new calendars.
If a user is the owner of a calendar, then he can manage the calendar and all the administrations that belong to this calendar, including his own administration.
If a user is editor of a calendar, then he can read and update this calendar, and destroy his administration.
If a user is viewer of a calendar, then he can read this calendar, and destroy his administration.
To implement the above, I have come up with the following ability.rb file:
class Ability
include CanCan::Ability
def initialize(user, calendar)
user ||= User.new
calendar = Calendar.find(params[:id])
user can :create, :calendar
if user.role?(:owner)
can :manage, :calendar, :user_id => user.id
can :manage, :administration, :user_id => user.id
can :manage, :administration, :calendar_id => calendar.id
elsif user.role?(:editor)
can [:read, :update], :calendar, :user_id => user.id
can :destroy, :administration, :user_id => user.id
elsif user.role?(:viewer)
can [:read], :calendar, :user_id => user.id
can :destroy, :administration, :user_id => user.id
end
end
end
Since I am not very experimented with Rails and it is the first time I am working with CanCanCan, I am not very confident with my code and would like some validation or advice for improvement.
So, would this code work, and would it allow me to achieve what I need?
UPDATE: with the current code, when I log in as a user, and visit the calendars#show page of another user's calendar, I can actually access the calendar, which I should not.
So, obviously, my code is not working.
Any idea of what I am doing wrong?
UPDATE 2: I figured there were errors in my code, since I was using :model instead of Model to allow users to perform actions on a given model.
However, the code is still not working.
Any idea of what could be wrong here?
UPDATE 3: could the issue be caused by the fact that I use if user.role?(:owner) to check if a user's role is set to owner, while in the database the role is actually defined as "Owner" (as a string)?
UPDATE 4: I kept on doing some research and I realized I had done two mistakes.
I had not added load_and_authorize_resource to the calendars and administrations controllers.
I had defined two attributes two parameters — initialize(user, calendar) — instead of one in my initialize method.
So, updated both controllers, as well as the ability.rb file as follows:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role?(:owner)
can :manage, Calendar, :user_id => user.id
can :manage, Administration, :user_id => user.id
can :manage, Administration, :calendar_id => calendar.id
elsif user.role?(:editor)
can [:read, :update], Calendar, :user_id => user.id
can :destroy, Administration, :user_id => user.id
elsif user.role?(:viewer)
can [:read], Calendar, :user_id => user.id
can :destroy, Administration, :user_id => user.id
end
end
end
Now, when I try to visit a calendar that does not belong to the current_user, I get the following error:
NoMethodError in CalendarsController#show
undefined method `role?' for #<User:0x007fd003dff860>
def initialize(user)
user ||= User.new
if user.role?(:owner)
can :manage, Calendar, :user_id => user.id
can :manage, Administration, :user_id => user.id
can :manage, Administration, :calendar_id => calendar.id
How I can fix this?
There is no such method role? the User model. The Cancancan documentation is at fault for assuming such a method exists in the examples.
To fix this, you should instead do:
if user.role == 'Owner'
...
elsif user.role == 'Editor'
...
elsif user.role == 'Viewer'
...

Rails: Using CanCan to define multiple roles depending on instances of single Model?

I'm currently stuck on how to separate roles for CanCan depending on each condition that we want.
In our application, there are many categories (such as math, english, history, etc.) and within each are many courses.
Each user can have many different roles on each category. For example, John can be a "reader" for math, which means he can read all the courses that are in math. John can also be a "writer" for english, which means he can read all the courses in english, create a course within category english, and edit/delete only the courses he created within english.
If these were the only roles John had, he would not be able to see the category history in the navbar, and would be denied access to courses that are within history.
These are how relations are set up:
class User < ActiveRecord::Base
has_many :roles
def has_role?(role_sym)
roles.any? { |r| r.level.underscore.to_sym == role_sym }
end
end
class Category < ActiveRecord::Base
has_many :roles
has_many :courses
end
class Role < ActiveRecord::Base
belongs_to :user
belongs_to :category
attr_accessible :level, :category_id, :user_id
end
in model/ability.rb we have
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in) #guest
if user.has_role? :reader
reader(user)
end
if user.has_role? :writer
writer(user)
end
end
#BE ABLE TO SEE COURSES AND CATS FOR PERMITTED CATS.
def reader(user)
can :read, Category, :roles => { :user_id => user.id, :level => "reader" }
## how would we be able to limit reading of courses that are within permitted categories? something like category.courses ~~
end
def writer(user)
reader(user) #inheriting from reader? this doesnt work because level is hardcoded into reader
can :read, Category, :roles => { :user_id => user.id, :level => "writer"}
# 1.you can read all courses in category that you are given permission to
# 2.you can write courses in permitted category
# 3.you can edit, delete courses that only youve created within permitted category
end
end
Questions:
How do we separate the roles of "reader" and "writer" in the correct way? How do we access the courses that are within the categories that we have access to?
After defining the reader and writer methods in the ability.rb, how do we use them in our view pages? It looks like the current documentations use something like "<% if can? :read, #category %>
" but that doesn't use the methods we separated and defined.
p.s. We will have 7 different roles: guest, reader, writer, editor, manager, admin, and app_admin(our developers)
I've been trying to solve this for 3 days now - please understand that I'm still fairly a beginner! Thanks in advance
I ran into a same needs today and found a way to do this on CanCan Wiki.
Simple follow these simple steps:
1) Create a constant under the User class with your role names:
class User < ActiveRecord::Base
ROLES = %w[admin moderator author banned]
end
2a) Create and run a migration if you are using ActiveRecord:
rails generate migration add_roles_mask_to_users roles_mask:integer
rake db:migrate
2b) Add these field on User model if you are using Mongoid:
field :roles_mask, type: Integer
3) Next you'll need to add the following code to the User model:
# in models/user.rb
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.inject(0, :+)
end
def roles
ROLES.reject do |r|
((roles_mask.to_i || 0) & 2**ROLES.index(r)).zero?
end
end
4) If you're using devise without strong parameters, don't forget to add attr_accessible :roles to you user model. If you're using devise with strong_parameters, either as a gem in a Rails 3 app, or as is built-in in Rails 4, dont forget to add the roles to the permitted list in the controller:
class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) {|u| u.permit(:email, :password, :password_confirmation, roles: [])}
end
end
5) Add the code bellow to generate checkboxes in the view for setting these roles:
<% for role in User::ROLES %>
<%= check_box_tag "user[roles][#{role}]", role, #user.roles.include?(role), {:name => "user[roles][]"}%>
<%= label_tag "user_roles_#{role}", role.humanize %><br />
<% end %>
<%= hidden_field_tag "user[roles][]", "" %>
6) Finally, you can then add a convenient way to check the user's roles in the Ability class:
# in models/user.rb
def is?(role)
roles.include?(role.to_s)
end
# in models/ability.rb
can :manage, :all if user.is? :admin
That's it.
I hope this can help.
In your gemfile Include.
gem "cancan"
install bundle.
rails g cancan:ability
this will generate an ability class in your models.
define your Abilities there like below.
but keep remember that you have already defined roles,
such as you have a User model,
having two roles defined i.e admin and support.
class Ability
include CanCan::Ability
def initialize(user)
user||= User.new
can :read, :all
if user.role == 'admin'
can :manage, :all
else
can :read, :all
end
end
end
4. the resource on which you want to restrict a user,
use the following filter in their controller.
load_and_authorize_resource
5. if you want restrict something in the views not to show.
<% if can? :manage, #flower %>
<td><%= link_to 'Edit', edit_flower_path(flower) %></td>
<% end %>
<% if can? :manage, #flower %>
<td><%= link_to 'Destroy', flower_path(flower), method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>

Cancan :create ability

I have the following code:
#/app/models/users/user.rb
class Users::User < ActiveRecord::Base
has_many :phones, class_name: "Users::Phone"
end
#/app/models/users/phone.rb
class Users::Phone < ActiveRecord::Base
belongs_to :user, class_name: "Users::User"
attr_accessible :phone
end
#/app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
can :read, :all
unless user.nil? #logged_in
if user.is? :admin
can :manage, :all
else
can :create, Users::Phone, user_id: user.id
end
end
end
end
I wanna check ability for create only their own phones for users
#/app/views/users/users/show.html.slim
- if can? :create, Users::Phone.new
a[href="#{new_user_phone_path(#user)}"] Add phone
Thats does not work, because I should pass user_id to phone model (like Users::Phone.new user_id: user.id), but I can't do that since Phone's mass assignment.
So how I can check :create phones ability for users?
I do something similar to this in my app by making Ability aware of the underlying parameter structure. You have a few options depending on your requirements. So in your controller you'd have approximately:
def create
#phone = Users::Phone.new(params[:users_phone])
# Optional - this just forces the current user to only make phones
# for themselves. If you want to let users make phones for
# *certain* others, omit this.
#phone.user = current_user
authorize! :create, #phone
...
end
then in your ability.rb:
unless user.nil? #logged_in
if user.is? :admin
can :manage, :all
else
can :create, Users::Phone do |phone|
# This again forces the user to only make phones for themselves.
# If you had group-membership logic, it would go here.
if phone.user == user
true
else
false
end
end
end
end

rails and cancan - how to restrict the company show action to users owner in has_many through association

i'm trying to solve an user's ability problem with cancan gem.
company and users are associated through user_company_assignment in such a way that a company has many user and the users has and belongs to many companies
I would like to restrict the show action of a company only to those users associated with the company. below there is the code of the two models and a snip of ability.rb with the initialize role inheritance and the method for the seller user, but this is not working, it show me always the company detail.
Company.rb
has_many :user_company_assignments
has_many :user, :through => :user_company_assignments
User.rb
has_many :user_company_assignments
has_many :companies, :through => :user_company_assignments
Ability.rb
def initialize(user)
#user = user || User.new # for guest
#user.roles.each { |role| send(role.name.downcase) }
end
def seller
can :manage, :all
cannot :destroy, :all
can :show, Company do |company|
company.user_ids.include? #user.id
end
end
Your error is due to ability precedence: https://github.com/ryanb/cancan/wiki/Ability-Precedence
This line overrides all following abilities: can :manage, :all
Since you've already stated that a seller can manage all, the seller can perform any kind of action on a Company, regardless of the other can statement.
One solution would be to use cannot, as you did with :destroy. It will override the :manage, :all clause.
def seller
can :manage, :all
cannot :destroy, :all
cannot :show, Company do |company|
!company.user_ids.include? #user.id
end
end
You need to call the load_and_authorize_resource method in your controller.
Ok jesper, i have changed my ability.rb and it works but i'm not sure that this is the best method to set the ability, it is strange the i need to specify each Models that a seller has the permission to the show action. tell me if is it the best way to do that:
Ability.rb
def seller
can [:index, :create], :all
cannot :destroy, :all
can :show, Company do |company|
company.user_ids.include? #user.id
end
can :show, [Report, Client]
end

Cancan - new form fails when user restricted to certain sites

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

Resources