I'm new to rails, and I'm working on my second rails app.
The app will have different roles for users, but some users will have multiple roles.
Every user of the site will be an Artist. Some users will have the role of a moderator.
How would I structure this? In some PHP apps I've used, there is only one user, and then a database column for is_admin, etc. But I've looked at the source for rails apps and have seen separate models for User and Admin, etc. although I'm not sure why.
So, should I have a single User model with a role attribute, which could be Moderator, and then just call Users "Artists" in my views, routes, etc.?
Or should I have a User model, a Moderator model which inherits from it, and an Artist model which belongs_to User?
I'm really confused.
You can look for gems Devise and CanCan. This pair is really powerful combination. This makes two models User and Role. In Role you can create new roles, without creating new models for them. Although it creates model Ability, here you can define access rules for roles.
Manual:
http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/
Here you can find Devise's and CanCan's sources and wikies:
https://github.com/plataformatec/devise
https://github.com/ryanb/cancan
My models looks like this:
Role.rb
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
User.rb
class User < ActiveRecord::Base
has_many :accounts
has_and_belongs_to_many :roles
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, :lockable and :timeoutable
devise :database_authenticatable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :username, :password, :password_confirmation, :remember_me, :role_ids
def role?(role)
return !!self.roles.find_by_name(role.to_s.camelize)
end
end
Ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user
if user.role? :administrator
can :manage, :all
elsif user.role? :operator
can :read, Account
can :read, Server
elsif user.role? :customer
can :manage, Account
can :read, Server
end
end
end
In the controller you must add only this two lines:
class YourController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource
...
end
If you need to have code that's specific to and role or such as an admin or moderator one other solution would be to create a base User model that all the other classes inherit from. You can then create an Admin class and a Moderator class that inherit from the User model. This would mean you can avoid constantly checking the users role in your code e.g. current_user.do_some_admin_thing if current_user.is_admin?. Your classes would look something like this
class User < ActiveRecord::Base
# base user methods in here
end
class Moderator < User
def do_moderator_thing
# perform a moderator task
end
end
class Admin < Moderator
def do_admin_thing
# perform an admin task
end
end
In this instance the User class has the most basic privileges, moderators can do everything users can plus the moderator specific methods and admins can do everything users and moderators can plus the admin specific methods.
All the different user roles would use the same table in the database but your concerns are neatly separated into classes which avoids excessive conditionals through your code checking what role the user is all the time.
Creating new users would be straightforward also Admin.new :name => 'bob' the Admin class then takes care of how a user is defined as an admin which provides a nice interface where you don't need to know the inner workings of the role system to interact with users.
I think you don't have to create different models because you don't have specific fields for each one. So you just have to set the "role" of each User. Two options : create a role table or add a role field in the table User. Both solutions work, the second is more flexible but less optimized.
But, in your particular case, you don't have a complex role management so you can find a simpler solution. If all of your users are artists you don't have to specify this in your code, it's contained in the implicit description of what a user is. So you just have to save if a user is an admin or not and I think the best solution is to create a boolean field "is_admin".
After that you will have to create some before_filter in your protected controllers, like that :
before_filter => :authorize, :only => :new, :edit, :create, :update, :destroy
def authorize
redirect_to :root if not current_user.is_admin?
end
And you can have simple requests like that :
#artists = User.all
#moderators = User.where(:is_admin => true)
If you look for a more complete authorization system you can check this small gem : https://github.com/ryanb/cancan
But I think it's not the case for the moment. If you have a simple problem look for a simple solution !
Although I agree the combination of Devise and CanCan is powerful and works. Let us look at it with a different perspective keeping Association and Delegation in mind.
Association: In object-oriented programming, association defines a
relationship between classes of objects that allows one object
instance to cause another to perform an action on its behalf.
Delegation: Delegation allows the behaviour of an object to be
defined in terms of the behaviour of another object. The term
'delegation' refers to the delegation of responsibility. The primary
emphasis of delegation is on message passing where an object could
delegate responsibility of a message it couldn't handle to objects
that potentially could (its delegates).
With that, what if we design our User and Roles like this. There is no Role class and the User doesn't inherit or specialise a particular class ( Artist, Admin ) instead, all the (Role) class contain the User object and delegate. The way I am thinking and way to implement with Rails is something like this:
class User < AR::Base
def user_method
end
end
class Artist < AR::Base
has_one :user
def artist_method
# perform an admin task
end
end
class Admin < AR::Base
has_one :user
def admin_method
# perform an admin task
end
end
This role class model is described by Francis G. Mossé in his article on Modelling Roles.
This is the basic setup, for the declarative authorization gem, I use. But you could just use this as is without the gem, if your authorization requirements aren't more than asking the kind of Roles the User has.
It does require a roles table, and such, so that might not really be your fancy.
class Role < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :roles
def role_symbols
roles.map { |r| r.title.to_sym }
end
def admin?
has_role?(:admin)
end
# include more role booleans or write some ruby magic to be DRY
# ...
def has_role?(r)
role_symbols.include?(r.to_sym)
end
end
# give or take
user = User.new
user.roles << Role.new :title => "admin"
user.roles << Role.new :title => "artist"
user.role_symbols # => [:admin, :artist]
user.admin? # => true
user.has_role?(:artist) # => true
You can have two models User and Role. And Role belongs to User.
Specify role of users (like admin, moderator) in Role model.
Related
I'm creating a Rails application where users can sign up by checking a box in a form where they are either a "person" or "organization". I'm struggling to find a way to implement this into Rails. Both of these user types would have the same authorization. I have no idea if I want to use a string or a boolean as a data type in my ActiveRecord database. Also, what would I need to put in my model (User.rb) and my controller in order to validate it and implement it respectively?
There are many ways to implement this; it depends on what your needs are. Ask yourself: "Do people and organizations share the same attributes?"
AR Enum
If they do, the only thing that differentiates the two is role (or whatever you want to call it), i.e., person or organization. For that scenario, Rails 4.1 provides AR enums. This is the simplest solution, it could go something like this:
class User < ActiveRecord::Base
enum role: [ :person, :organization ] # #user.role => 'person', #user.person? => true
end
Polymorphic Association
On the other hand, if people and organizations share only some attributes, you might consider using a polymorphic association (If people and organizations share no attributes—not even role—they should be two different models). The base model should contain the attributes that both people and organizations share. The person/organization models should contain attributes specific to that model.
# common attributes
class User < ActiveRecord::Base
belongs_to :profile, polymorphic: true
def self.roles
%w(person organization)
end
end
# person-specific attributes
class PersonProfile < ActiveRecord::Base
has_one :user, as: :profile, dependent: :destroy
end
# organization-specific attributes
class OrganizationProfile < ActiveRecord::Base
has_one :user, as: :profile, dependent: :destroy
end
For user signup, you can create users#new and users#create actions. In your user signup form (perhaps app/views/users/new.html.erb), you could use a select_tag to let the user specify their role. Then, use that to determine what kind of profile to attach to your user model. For example (users#create):
def create
#user = User.new(user_params)
if role = params[:role]
# return HTTP 400
head :bad_request and return unless User.roles.include?(role)
# Assign the User's profile
#user.profile = "#{role.capitalize}Profile".constantize.new
else
# enter your own logic here
end
#user.save ? redirect_to(#user) : render(:new)
end
The handling of sessions (user signin/signout), in my opinion, should be handled in a separate SessionsController.
Add a new table to the database named user_types with fields role and id. And in users table you need to add user_type_id column. Then, in UserType model
class UserType < ActiveRecord::Base
has_many :users
end
And in User model you need to add
class User < ActiveRecord::Base
belongs_to :user_type
end
You can create UserType records in seeds, but make sure it runs everytime the database is reset, add this to seeds.rb
UserType.create!(role: "person")
UserType.create!(role: "organization")
Hope this makes sense!
If you have only two types of users (Person and Organization) as indicated in your question, you could just have a Person model, and add a bool field is_organization to it. For more details, See how devise, a popular authentication gem, handles this here (this approach is option 2 on the linked page, you can also check out option 1, which is to create an entire new model).
I'm trying to build a multi tenanted app in which which different banks are separated by subdomain. This part is working fine. Now there is one more level of multitenancy for bank products.
Each bank has multiple products
A devise user can belong to only on product
This means that you will have to register twice for two products of the same bank even though they are under same subdomain(client requirement can't change)
Because of this you can have same email address for two products. Uniqueness is scoped to product_id
So I have to select a product while signing in and signing up
This is how I'm trying to implement above solution
around_filter :scope_current_bank, :scope_current_product
before_filter :authenticate_user!
helper_method :current_bank, :current_product
def current_bank
#current_bank = Bank.find_by_subdomain!(request.subdomains.first)
end
def current_product
if user_signed_in?
#current_product = current_bank.products.find_by_id(params[:product_id])
else
#current_product = current_user.product
end
end
def scope_current_bank
Bank.current_id = current_bank.id
yield
ensure
Bank.current_id = nil
end
def scope_current_product
Product.current_id = (current_product.id rescue nil)
yield
ensure
Product.current_id = nil
end
Now the problem is while user is sigining in, the scope_current_product method calls user_signed_in?, obviously it fails because product_id is nil. Now it enters the else block after which I expect it to call authenticate_user! as its a before_filter but it does not happen as authentication was already done. So I get a message saying authentication failed.
Is their any way to call authenticate_user again?
Although not a direct answer, hopefully this will give you some ideas:
Authorization
Perhaps you should look at - Is there a difference between authentication and authorization? - there's a good RailsCast about this
I think your issue comes down to the idea you need to authenticate the user once (login / logout), but should then authorize that user to work with different resources
Code
A devise user can belong to only on product - I would recommend this:
#app/models/product_user.rb
Class ProductUser < ActiveRecord::Base
belongs_to :product
belongs_to :user
end
#app/models/product.rb
Class Product < ActiveRecord::Base
has_many :product_users
has_many :users, through: :product_users
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :product_users
has_many :products, through: :product_users
end
This is a typical has_many :through association:
#user.products
#product.users
CanCan
It means you can use CanCan to do something like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user
can :manage, Product, users.exists?(user.id)
else
can :read, :all
end
end
end
This allows you to control which products the user can edit / access. Obviously my code needs to be tweaked, but I hope it shows you the value of authorization over trying to do multiple authentications
I am using CanCan and have been researching how to get started. However, it seems that most of the tutorials aren't very specific, and don't suit my own needs. I'm building a social network where users can create projects and add other users to their projects, allowing those users to moderate that project.
I currently have a Role model with a string attribute, and a User model from devise. Where do I go from here?
I have seen this post, but it doesn't fully explain how to set up the roles and the relationship between the Role model and the ability.rb file from CanCan.
If you need me to be more specific, please say so! I'm not the greatest Rails developer ;)
Edit
I have seen the railscast on this, and it doesn't have a separate Role model which I would like to have. I have tried using Rolify, but people have said it is too complicated and that it's possible to do it in a more simple way. I also ran into some complications so I'd like to just use my own Role model.
Edit
I'm currently using rolify and the roles are working. I found my solution at: https://github.com/EppO/rolify/wiki/Tutorial
If your User-Role stuff is looking similar to following:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
# user model has for example following attributes:
# username, email, password, ...
end
class Role < ActiveRecord::Base
has_many :user_roles
has_many :users, :through => :user_roles
# role model has for example following attributes:
# name (e.g. Role.first.name => "admin" or "editor" or "whatever"
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
you can do following:
First, extend your User model with a few helper methods or something similar:
class User < ActiveRecord::Base
def is_admin?
is_type?("admin")
end
def is_editor?
is_type?("editor")
end
def is_whatever?
is_type?("whatever")
end
private
def is_type? type
self.roles.map(&:name).include?(type) ? true : false # will return true if the param type is included in the user´s role´s names.
end
end
Second, extend your ability class:
class Ability
include CanCan::Ability
def initialize(user)
if user
can :manage, :all if user.is_admin?
can :create, Project if user.is_editor?
can :read, Project if user.is_whatever?
# .. and so on..
# you can work with your different roles on base of the given user instance.
end
end
end
Alternatively you could remove your User-Roles has-many-through associations and replace it with the easy-roles gem - very useful. It is available on github: https://github.com/platform45/easy_roles
Now you should have an idea how you could work with cancan, roles and all the stuff together :-).
I have a Group model that has many members (User models).
class Group < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
has_many :members, :through => :group_members, :class_name => 'User'
end
The User model is using Devise. I need to add the ability for a User (Group Owner) to "invite" another User (who may or may not have a User record yet) to belong to the Group. How should I go about doing this? Has something like this already been built and packaged as a Gem?
Well, first action would be to find the user and then add him to the group of he exists. If he does not exist, do something like send an invite by email and put that invitation into a separate table, also belonging to the group.
Then, if someone with that same email address signs up, put the new user directly into the group. In total: Add a new model named like "invited_user" which only has an email address row and belongs to the group model.
class InvitedUser < ActiveRecord:Base
belongs_to :group
end
Create an invite action like this:
def invite_user
user = User.find_by_email(params[:email])
group = Group.find(params[:id])
if user
group.users << user
else
send_invite user.email
group.invited_users << user
end
end
And finally you need to subclass Devise's registration controller, so you can override/add to the default action after a successful sign up. However, this part may not be reliable since I'm partly relying on Devise's documentation and did not try it out myself:
class RegistrationsController < Devise::RegistrationsController
protected
def def after_sign_up_path_for(resource)
invited_user = InvitedUser.find_by_email(resource.email)
if invited_user
invited_user.group.users << resource
invited_user.destroy
end
after_sign_in_path_for(resource)
end
end
Or something like that. And you still need to implement the send_invite action, of course
I'm using Devise, and for each User account created I want to generate a relationship where:
class User < ActiveRecord::Base
belongs_to :business
end
class Business < ActiveRecord::Base
has_many :users
has_one :apt_setting
has_many :hours, :as => :hourable
end
class ApptSetting < ActiveRecord::Base
belongs_to :business
end
So upon registration an associated Business object is created, and with each Business object an associated ApptSettings and BusinessHour object is created.
I currently have this implemented like this:
class Admin
before_create :create_associated_records
def create_associated_records
# create the associated business object
business = Business.create(:business_name => business_name, :subdomain => subdomain, :initial_plan => initial_plan)
# retrieve the id of the new business object
self.business_id = business.id
# create the associated records
BusinessHour.default_values(business_id)
ApptSetting.default_values(business_id)
end
end
class ApptSetting < ActiveRecord::Base
belongs_to :business
def self.default_values(business_id)
# ... create record with default values
end
end
class BusinessHour < Hour
belongs_to :hourable, :polymorphic => true
def self.default_values(business_id)
# ... create record with default values
end
end
This does work, but does it seem like the best design?
One alternative I'm considering is handling removing Admin -> create_associated_records, and instead do that work in Users::Accounts::RegistrationsController where I override the 'create' method. There I could build all the associated records, set :accepts_nested_attributes where appropriate, then call 'save' on the Business object, which should then cause all the associated records to be generated.
Thoughts on the best design, or any other ideas?
you don't need the default_values methods. In your create_associated_records you can change those calls to:
ApptSetting.create(:business_id => business_id)
Don't override the create method. before_create callbacks are a better way to go. In either case, If a business has many users, do you really want to create a new business every time a new user is created? How does a second user ever get added to a business? add something like,
def create_associated_records
return unless self.business_id.nil?
....
Also where are the business_name, subdomain, and initial_plan variables coming from in your method? Do you have them as attributes of the admin user? Seems like they should be only values of the business.
I think the biggest question here is, does a user really need a business in order to exist? Why can't the user just create their Business after they create their account?
** Edit: Being more clear / cleaner version using rails association methods:
class Admin
before_create :create_associated_records
private
def create_associated_records
return unless self.business_id.nil?
self.create_business
self.business.create_appt_setting
self.business.hours.create
end
end