This is a follow-up question on Rails 4: CanCanCan abilities with has_many :through association and I am restating the problem here since I believe context has slightly changed and after 4 updates, the code from the initial question is pretty different too.
I also checked other questions, like Undefined method 'role?' for User, but it did not solve my problem.
So, here we go: I have three 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 defined in the administration join model (in a column named role).
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)
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 log in and try to visit any calendar page (index, show, edit), 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
I guess the problem comes from the fact that a user does not have a role per se, but only has a role defined for a given calendar.
Which explains why I get a NoMethodError for role? on user.
So, the question would be: how to check a user role for a given calendar?
Any idea how to make things work?
You should have role? method in user model, like below -
class User < ActiveRecord::Base
has_many :administrations
has_many :calendars, through: :administrations
def role?(type)
administrations.pluck(:role).include?(type.to_s)
end
end
Related
The Setup
I'm using Rails 5.2 with the cancancan gem.
rails g scaffold User first_name email:uniq
rails g scaffold Organization name:uniq
rails g scaffold Role name
rails g scaffold Membership user:references organization:references role:refences
user.rb
class User < ApplicationRecord
has_many :memberships
has_many :roles, through: :memberships
has_many :organizations, through: :memberships
end
membership.rb
class Membership < ApplicationRecord
belongs_to :role
belongs_to :organization
belongs_to :user
end
organization.rb
class Organization < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
role.rb
class Role < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
end
seeds.rb
admin = Role.create(name: 'Admin')
user = Role.create(name: 'User')
abc = Organization.create(name: 'Abc Inc.')
bob = User.create(first_name: 'Bob')
alice = User.create(first_name: 'Alice')
Membership.create(role: user, company: abc, role: user)
Membership.create(role: admin, company: abc, role: admin)
The Task
An admin should be able to manage all users and memberships of the company he/she is admin for. A user can only read all users and memberships of that company.
Here is my take on a cancancan configuration:
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
user_role = Role.find_by_name('User')
admin_role = Role.find_by_name('Admin')
organizations_with_user_role = Organization.includes(:memberships).
where(memberships: {user_id: user.id, role_id: user_role.id})
organizations_with_admin_role = Organization.includes(:memberships).
where(memberships: {user_id: user.id, role_id: admin_role.id})
can :read, Organization, organizations_with_user_role
can :manage, Organization, organizations_with_admin_role
end
end
Then I try to run this code in a view:
<% if can? :read, organization %><%= link_to 'Show', organization %><% end %>
This results with an error page which says:
The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for :read #
I guess I'm tackling the problem from a totally wrong angle. How do I have to setup the ability.rb to solve this problem?
I'd use a simple hash of conditions here:
can :read, Organization, memberships: { user_id: user.id, role: { name: 'User' } }
can :manage, Organization, memberships: { user_id: user.id, role: { name: 'Admin' } }
this should be really enough and you have the advantage that, with this syntax, you can also call Organization.accessible_by(ability, :read) and retrieve all the Organizations that a user can read.
From the documentation:
Almost anything that you can pass to a hash of conditions in Active
Record will work here. The only exception is working with model ids.
You can't pass in the model objects directly, you must pass in the
ids.
can :manage, Project, group: { id: user.group_ids }
So try something like:
can :read, Organization, id: organizations_with_user_role.pluck(:id)
On a separate note, why are you using includes instead of joins? Your query can be simplified to (without the need for user_role = Role.find_by_name('User')):
organizations_with_user_role = Organization.joins(memberships: :role).
where(memberships: {user_id: user.id}).where(roles: {name: 'User'})
I have an application where a user adds a subscription and an account is automatically created for that subscription. I also want to pass the current user to the account model as the account_manager. So far I have:
class Subscription < ActiveRecord::Base
has_one :account
after_create :create_account #after a subscription is created, automatically create an associated account
def create_account
Account.create :account_manager_id => "1" #need to modify this to get current user
end
end
class Account < ActiveRecord::Base
has_many :users
belongs_to :account_manager, :class_name => 'User', :foreign_key => 'account_manager_id'
belongs_to :subscription, :dependent => :destroy
end
This works fine for the first user obviously but any attempts I've made to pass current_user, self, params, etc fails. Also when I use the def method the subscription ID is no longer passed to the account. I tried passing the current user through the AccountController but nothing happens. In fact I can still create an account if my AccountController is completely blank. Is after_create the best way to create an associated account and how do I pass the user to the account model? Thanks!
If you are using devise, you can do this directly in the controller with the current_user helper without a callback:
# subscriptions_controller.rb
def create
...
if #subscription.save
#subscription.create_account(account_manager: current_user)
end
end
I'm trying to create a review system on users in rails. I want one user to be able to rate another user on their profile page in devise. I've tried a few different methods but I am fairly new to rails and haven't been able to accomplish this.
Right now I have default devise views but no user profile page. I'd like users to review a another user on 5 or so different issues.
Any help would be much appreciated!
In order to do that, you can use the association called has_many through association :
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Your models should look like that "
class User < ActiveRecord::Base
has_many :rates
has_many :rated_users, through: :rates, class_name: "User", foreign_key: :rated_user_id # The users this user has rated
has_many :rated_by_users, through: :rates, class_name: "User", foreign_key: :rating_user_id # The users that have rated this client
end
class Rates < ActiveRecord::Base
belongs_to :rating_user, class_name: "User"
belongs_to :rated_user, class_name: "User"
end
And your migrations :
class createRates < ActiveRecord::Migration
def change
create_table :changes do |t|
t.belongs_to :rated_user
t.belongs_to :rating_user
t.integer :value
t.timestamps
end
end
end
Oxynum - great concept! After adding models and applying migrations, starts with templates. Starting point for you is a users_controller.rb. Probably, you already have a 'show' action inside UsersController. This action available for authenticated users.
Modify this action to smth like:
class UsersController < ApplicationController
before_filter :authenticate_user!
before_filter :load_ratable, :only => [:show, :update_rating]
def show
# Renders app/views/users/show.html.erb with user profile and rate controls
end
def update_rating
my_rate_value = params[:value] == 'up' ? +1 : -1
if #rated_by_me.blank?
Rate.create(rated_user: #userProfile, rating_user: #user, value: my_rate_value)
flash[:notice] = "You rated #{#userProfile.name}: #{params[:value]}"
else
flash[:notice] = "You already rated #{#userProfile.name}"
end
render action: 'show'
end
protected:
def load_ratable
#userProfile = User.find(params[:id]) # - is a viewed profile.
#user = current_user # - is you
#rated_by_me = Rate.where(rated_user: #userProfile, rating_user: #user)
end
end
Add to routes:
get 'users/update_rating/:value' => 'user#update_rating'
Start rails server, Log In, and try to change rating directly:
http://localhost:3000/users/update_rating/up
I have followed this tut http://railsapps.github.com/tutorial-rails-bootstrap-devise-cancan.html I want to do something like this:
before_filter :authenticate_user!
before_filter :authenticate_VIP!
before_filter :authenticate_admin!
before_filter :authenticate_somerole!
I have tables: roles, users, user_roles and I don't want to create another table (rails g devise VIP create another table).
I want to have methods authenticate_ROLE. How to do this ?
I have three table, Users, Roles, and RoleRelationships (or role_users, it's up to you)
This is my Role table:
class Role < ActiveRecord::Base
attr_accessible :name
has_many :role_relationships
has_many :users, through: :role_relationships
end
Role table will have name column for roles, like: "admin", "teacher", "vip" (as you want).
And this is User table:
class User < ActiveRecord::Base
devise ...
has_many :role_relationships
has_many :roles, through: :role_relationships
end
and my RoleRelationship table:
class RoleRelationship < ActiveRecord::Base
attr_protected :role_id, :user_id
belongs_to :user
belongs_to :role
end
I set up my app one user can have many roles, you can set up your way. So, i have a role?(role) method in my user.rb, like this:
def role?(role)
return role == RoleRelationship.find_by_user_id(self.id).role.name
end
Then in my abilities files, i define abilities of users:
def initialize(user)
user ||= User.new # guest user
if user.role? "teacher"
can :read, Course
can :manage, Topic, user_id: user.id
can :create, Topic
else user.role? "admin"
can :manage, Course
end
So, teacher will only read Course, and admin can CRUD Course. To do that, i use method load_and_authorize_resource in my CoursesController:
class CoursesController < ApplicationController
load_and_authorize_resource
before_filter :authenticate_user!
...
end
Finally, in my views, i used code like this:
<% if can? manage, #course %>
Only admin can work, see what happen here.
<% end %>
So, as you see, teacher only can read Course so they can't see or do what admin can do, in this case, is create course or edit course.
This is what i built in my online test app, you can reference and do the same for your app.
Is there no-one out there who can assist with this?
Having problems finding records using cancan in my rails 3.2 app.
In my 'nas' index, I'm trying to display a list of 'nas' that belong to the current_user's locations.
The issue seems to be that there's no formal relationship between the user and the nas - the location owns the nas and the user owns the location.
Using the accessible_by method in my nas controller gives me unusual results. If I use the following in my ability.rb, I get an error :
can :read, Nas, :locationusers => { :user_id => user.id }
Error: undefined method `class_name' for nil:NilClass
And, if I change to:
can :read, Nas, :locations => { :user_id => user.id }
I only get the nas listed for the users first location.
For example, if my user has locations with ids = 1,2,3 only the nas from location 1 are displayed.
Is there a way to display all the nas for the current user's locations using cancan or do I have to go about this differently?
My relationships are as follows:
class User < ActiveRecord::Base
...
has_many :locationusers
has_many :locations, :through => :locationusers
...
end
class Location < ActiveRecord::Base
...
has_many :locationusers
has_many :users, :through => :locationusers
has_many :nas
...
end
class Node < ActiveRecord::Base
...
belongs_to :location
end
In my ability.rb:
...
if user.role? :customer_admins
can :read, Nas, :locations => { :user_id => user.id }
..
NasController
#nas = Nas.accessible_by(current_ability).all
With Cancan, the User is king. If you cannot draw a line from User to the Model you are trying to authorize, it is not going to work. The easy solution is to model the relationship between Nas (the model in your example is called Node?) and User. This can be done via Locationuser (which is not shown in your example) as a has_many :through using the existing belongs_to.
class Node < ActiveRecord::Base
...
belongs_to :location
has_many :locationusers, :through => :location
end
Now in your Cancan ability.rb, you can use:
can :read, Node, :locationusers => { :user_id => user.id }
(I changed Nas from your example to Node to correctly match the model)