Rails, devise, acl - ruby-on-rails

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.

Related

Rails Devise - How to add more data to current_user

Suppose I have a User model
user.rb
class User < ActiveRecord::Base
...
end
with attributes like: name, username, access
access is an enum that tells me if the user is "staff" or "customer"
To get the name and username of the logged in user, I can do:
current_user.name
current_user.username
And suppose I have a Staff model
staff.rb
class Staff < ActiveRecord::Base
belongs_to :user
end
with attributes like: salary, phone_number
And I also have a Customer model
customer.rb
class Customer < ActiveRecord::Base
belongs_to :user
end
with attributes like: address, phone_number
I want to be able to call this on my staff's controller:
current_user.staff.salary
And this on my customer's controller:
current_user.customer.address
WHAT I TRIED SO FAR
I overwrote sessions_controller.rb
def create
super
model_name = current_user.access.capitalize.constantize
spec = model_name.where(user_id: current_user.id).take
session[:spec] = spec
end
So I'm able to access it via session[:spec], but not via current_user. Any ideas?
Well to begin with, your User model should reference the staff or customer, even if they are to stay blank
class User
has_one :staff
has_one :address
Just by doing this, you should be able to use current_user.customer.address. However...
I suggest you add some convenient methods in ApplicationController or a module that you include
def staff_signed_in?
#staff_signed_in ||= (user_signed_in? and current_user.access == :staff)
end
def current_staff
#current_staff ||= (current_user.staff if staff_logged_in?)
end
# same for customer
# Note that I use instance variables so any database queries are executed only once !
Then you can simply call
<% if customer_signed_in? %>
<h2>Logged in as customer</h2>
<p>Address : <%= current_customer.address %>
<% end %>
EDIT : about your concerns concerning database hits
You gave the example of current_user.customer.cart.products
This is indeed quite a nested association. My suggestion above already reduces it by one level (ie current_customer == current_user.customer). Then you have to go through carts to reach products... it isn't so bad in my opinion.
If you need to call that often (current_customercustomer.cart) you can override the current_customer for a given controller and eager load the resources you know you will use use.
def UserShopController < ApplicationController
# Let's assume current_customer is defined in ApplicationController like I showed above
# UserShopController always uses the customer cart, so let's load it right at the beginning
...
private
# Override with eager loading
def current_customer
#current_customer ||= (current_user.customer.includes(:cart) if customer_logged_in?)
end
add has_one :customer to your user.rb
Your user model should be like below to accessing related model.
class User < ActiveRecord::Base
has_one :customer
end

Rails 4 + CanCanCan: "undefined method `role?' for User"

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

When updating a nested form, how do I do something with new records?

class Photo < ActiveRecord::Base
has_many :item_photos
has_many :items, through: :item_photos
end
class Item < ActiveRecord::Base
has_many :item_photos
has_many :photos, through: :item_photos
accepts_nested_attributes_for :photos
end
class ItemPhotos < ActiveRecord::Base
belongs_to :photo
belongs_to :item
end
When I edit or create an Item, I also upload or remove Photos. However, more than one user can view an Item. These users should only be able to view their own Photos.
Item #1 has three Photos. Amy has access to one. Barry has access to two. Barry loads up /items/1 and edits it. He deletes one Photo, ignores the other, and adds two new Photos.
class ItemsController < ApplicationController
def update
if #item.update(item_params)
# Give Barry access to the Photo he just made.
#item.only_the_photos_barry_just_made.each do |p|
current_user.add_role :viewer, p
end
end
end
end
I don't want to pollute models/photo.rb with methods to access session information (like current_user). Is there an idiomatic way to get these records? If not, is there a clean way to get these records?
Thanks for any help.
A simple solution would be to add a :creator relation to photo.
rails g migration AddCreatorToPhotos creator:references
class Photo < ActiveRecord::Base
belongs_to :creator, class: User
# ...
end
And then you can simply add a rule in your Abilty class:
class Ability
include CanCan::Ability
# Define abilities for the passed in user here.
# #see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
def initialize(user = nil)
user ||= User.new # guest user (not logged in)
# ...
can :read, Photo do |photo|
photo.creator.id == user.id
end
end
end
You can then get the photos that can by read by the current user with:
#photos = #item.photos.accessible_by(current_ability)
Edit:
If you want to authorize though roles instead you just need to alter the conditions in the authorization rule:
class Ability
include CanCan::Ability
# Define abilities for the passed in user here.
# #see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
def initialize(user = nil)
user ||= User.new # guest user (not logged in)
# ...
can :read, Photo do |photo|
user.has_role? :viewer, photo
end
end
end
Edit 2:
An approach to creating the role could be to add a callback to Photo. But as you already have surmised, accessing the user via the session from a model is not a good approach.
Instead you can pass the to the user to Photo when it is instantiated. You can either setup the belongs_to :creator, class: User relationship or create a virtual attribute:
class Photo < ActiveRecord::Base
attr_accessor: :creator_id
end
You can then pass the user by a hidden field (remember to whitelist it!):
# GET /items/new
def new
#item = Item.new
#item.photos.build(creator: current_user) # or creator_id: current_user.id
end
<%= fields_for(:photos) do %>
<%= f.hidden_field :creator_id %>
<% end %>
So, how do we create our callback?
class Photo < ActiveRecord::Base
attr_accessor: :creator_id
after_commit :add_viewer_role_to_creator!, on: :create
def add_viewer_role_to_creator!
creator.add_role(:viewer, self)
true # We must return true or we break the callback chain
end
end
There is one issue though:
We don't want to allow malicious users to be assign their ID to existing photos on update.
We can do this by setting up some custom params whitelisting:
def item_params
whitelist = params.require(:item).permit(:foo, photos_attributes: [:a,:b, :creator_id]
current_user_photos = whitelist[:photos_attributes].select do |attrs|
attrs[:id].nil? && attrs[:creator_id] == current_user.id
end
whitelist.merge(photos_attributes: current_user_photos)
end

Rails Role model default if doesn't exist

We have a Role model that is connected to User via role_id, and there are some users that don't have roles assigned.
Would like to set a default Role on selecting users when they don't explicitly have a role set. Didn't setup a default in the migration.
Trying to use a loop:
users.each do |user|
# some users don't have roles so there are errors
end
The Role model:
class Role < ActiveRecord::Base
has_many :users
# Can I add something to set the default role
end
The user model:
class User < ActiveRecord::Base
belongs_to :role
end
Also if this is not a railsy way to approach this problem, any suggestions would be appreciated.
You could add an initialize method to the User model
after_initialize :init
def init
self.role =>'my_default_role'
end
You could also do a simple check for user.role_id before you do something role-related in your loop.
users.each do |user|
user.role = Role.find_by(name: 'user') unless user.role
end
Alternatively, you could modify the role method in your user class to add a default one if it's undefined.
class User < ActiveRecord::Base
belongs_to :role
def role
return :role if :role
Role.find_by(name: 'user')
end
end
Finally, you could just create a migration to add a role to all users who don't have one.

Devise User Review/Rating System in Rails 4

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

Resources