Rolify Gem: User has to have at least one role - ruby-on-rails

How can I validate presence of at least one role for a User using rolify gem? I tried validating presence of roles in User.rb as per below, but it does not work.
Bonus: Is it possible not to permit admin user take off his own Admin role?
User.rb:
class User < ApplicationRecord
rolify
validates :roles, presence: true
end
Edit Form:
= form_for #user do |f|
- Role.all.each do |role|
= check_box_tag "user[role_ids][]", role.id, #user.role_ids.include?(role.id)
= role.name
= f.submit
Controller:
class UsersController < ApplicationController
before_action :set_user, only: [:edit, :update, :destroy]
def edit
authorize #user
end
def update
authorize #user
if #user.update(user_params)
redirect_to users_path
else
render :edit
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit({role_ids: []})
end
end
When the user has 1+ roles it works ok, but if I take away all the roles it gives an error:

You can create a custom validation to requires that the user has at least one role:
class User < ActiveRecord::Base
rolify
validate :must_have_a_role
private
def must_have_a_role
errors.add(:roles, "must have at least one") unless roles.any?
end
end
The presence validation is really only intended for attributes and not m2m associations.
Is it possible not to permit admin user take off his own Admin role?
Its possible but will be quite complex since Rolify uses a has_and_belongs_to_many assocation and not has_many through: which would let you use association callbacks.

Related

Creating homes using nested routes

First this is all of my code
#models/user.rb
class User < ApplicationRecord
has_many :trips
has_many :homes, through: :trips
has_secure_password
accepts_nested_attributes_for :trips
accepts_nested_attributes_for :homes
validates :name, presence: true
validates :email, presence: true
validates :email, uniqueness: true
validates :password, presence: true
validates :password, confirmation: { case_sensitive: true }
end
#home.rb
class Home < ApplicationRecord
has_many :trips
has_many :users, through: :trips
validates :address, presence: true
end
class HomesController < ApplicationController
def show
#home = Home.find(params[:id])
end
def new
if params[:user_id]
#user = User.find_by(id: params[:user_id])
#home = #user.homes.build
end
end
def create
#user = User.find_by(id: params[:user_id])
binding.pry
#home = Home.new
end
private
def home_params
params.require(:home).permit(:address, :user_id)
end
end
I am trying to do something like this so that the home created is associated with the user that is creating it.
def create
#user = User.find_by(id: params[:user_id])
#home = Home.new(home_params)
if #home.save
#user.homes << #home
else
render :new
end
end
The problem is that the :user_id is not being passed into the params. So the #user comes out as nil. I can't find the reason why. Does this example make sense? Am I trying to set the associations correctly? Help or any insight would really be appreciated. Thanks in advance.
The way you would typically create resources as the current user is with an authentication such as Devise - not by nesting the resource. Instead you get the current user in the controller through the authentication system and build the resource off it:
resources :homes
class HomesController < ApplicationController
...
# GET /homes/new
def new
#home = current_user.homes.new
end
# POST /homes
def create
#home = current_user.homes.new(home_parameters)
if #home.save
redirect_to #home
else
render :new
end
end
...
end
This sets the user_id on the model (the Trip join model in this case) from the session or something like an access token when dealing with API's.
The reason you don't want to nest the resource when you're creating them as a specific user is that its trivial to pass another users id to create resources as another user. A session cookie is encrypted and thus much harder to tamper with and the same goes for authentication tokens.
by using if params[:user_id] and User.find_by(id: params[:user_id]) you are really just giving yourself potential nil errors and shooting yourself in the foot. If an action requires a user to be logged use a before_action callback to ensure they are authenticated and raise an error and bail (redirect the user to the sign in). Thats how authentication gems like Devise, Knock and Sorcery handle it.

Rails 5: permission for user to create record through permission table with Pundit

In my app I have Permission table, which stores all the logic what User can do. With Pundit I want to allow User to create new Campaign if Permission table allows. User can access Campaigns and create new, if Permission table contains this info:
permitable_type: Sysmodule // another table where I store info on System sections, where Campaigns is one of
permitable_id: 2 // means Campaigns from Sysmodule
level: 3 // means User can edit something in Campaigns section
So far I keep getting error "Pundit::NotDefinedError", unable to find policy of nil policies/application_policy.rb is standart, no changes.
Obviously I am doing sothing wrong. How do I do this authorization correctly? Many thanks for any help! I am on Rails 5 + Pundit.
models/permission.rb
class Permission < ApplicationRecord
belongs_to :permitable, polymorphic: true
belongs_to :user
enum level: {owner: 1, view: 2, edit: 3}
end
models/user.rb
has_many :permissions
has_many :campaigns, through: :permissions, source: :permitable, source_type: 'Campaign' do
def owner_of
where('`permissions`.`level` & ? > 0', Permission::owner )
end
end
has_many :sysmodules, through: :permissions, source: :permitable, source_type: 'Sysmodule' do
def can_access
where('`permissions`.`level` & ? > 1', Permission::can_access )
end
end
controllers/campaigns_controller.rb
def new
#campaign = Campaign.new
authorize #campaign
end
policies/campaign_policy.rb
class CampaignPolicy < ApplicationPolicy
attr_reader :user, :campaign, :permission
#user = user
#permission = permission
end
def new?
user.permission? ({level: 3, permitable_type: "Sysmodule", permitable_id: 2})
end
views/campaigns/index.html.erb
<% if policy(#campaign).new? %>
</li>
<li><%= link_to "New campaign", new_campaign_path(#campaign) %></li>
</li>
<% end %>
Instead of dealing directly with what permissions a user should have try thinking of it what roles users can have in a system.
This makes it much easier to create authorization rules that map to real world problems.
Lets imagine an example where we have users and groups. The rules are as follows:
groups can be created by any user
the user that creates the group automatically becomes an admin
groups are private
only admins or members can view a group
only an admin can modify a group
The models:
class User < ApplicationRecord
rolify
end
class Group < ApplicationRecord
resourcify
end
The policy:
class GroupsPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.with_roles([:admin, :member], current_user)
end
end
def show?
user.has_role?([:member, :admin], record)
end
def index?
true
end
def create?
true # any user can create
end
def new?
create?
end
def update?
user.has_role?(:admin, record)
end
def edit?
update?
end
def destroy?
update?
end
end
The controller
class GroupsController < ApplicationController
respond_to :html
before_action :autenticate!
before_action :set_group!, only: [:show, :edit, :update, :destroy]
def show
respond_with(#group)
end
def index
#groups = policy_scope(Group.all)
respond_with(#groups)
end
def new
#group = authorize( Group.new )
end
def create
#group = authorize( Group.new(group_attributes) )
if #group.save
current_user.add_role(:member, #group)
current_user.add_role(:admin, #group)
end
respond_with(#group)
end
def edit
end
def update
#group.update(group_params)
respond_with(#group)
end
def destroy
#group.destroy
respond_with(#group)
end
private
def set_group!
#group = authorize( Group.find(params[:id]) )
end
def group_params
params.require(:group).permit(:name)
end
end

When using devise in rails, how do I add two different types of users that will use the site in two different ways?

I'm building a job board application. I'm new to programming and am teaching myself the rails framework.
I'm using Devise for authentication. I will have two different types of users; Job Seeker and Employer. The job seeker will create a profile and search for job postings and the employer will create a company profile and post job listings. In the future, the employer will also be able to search for employees based on qualifications, experience, education, etc. but for now I'm just building my MVP.
This type of functionality is tricky, namely because you have to put functionality before implementation (IE most people get hung up about Devise, whereas it might not feature at all)
You have two ways:
Roles (authorization)
Multiple models (authentication)
Devise is an authentication system (user logged in); you may be better using authorization (can user do x or y). Authorization is out of Devise's scope.
Whilst you could use multiple models (Devise), I think it creates too much unnecessary bloat for what you need.
Instead, I would use a very simple role system (using enum):
#app/models/user.rb
class User < ActiveRecord::Base
enum role: [:job_seeker, :employer]
has_one :profile
before_create :build_profile
has_many :applications
has_many :listings, through: :applications
end
#app/models/application.rb
class Application < ActiveRecord::Base
belongs_to :listing
belongs_to :user
end
#app/models/listing.rb
class Listing < ActiveRecord::Base
has_many :applications
has_many :applicants, through: :applications, class_name: "User", foreign_key: :user_id
end
You'll need to add a role column (int) to your users table. You'll create the default role by using a default: [x] switch when creating your column:
def change
add_column :users, :role, :integer, default: 0 #-> defaults to job seeker
end
--
You've described several factors which would lend themselves perfectly to this:
Job seeker will create a profile
Employer will create a profile and post listings
... all meaning your "flow" will remain similar for both user types. You'd just have to manage what each user can do with authorization.
Setup
#config/routes.rb
resource :profile, controller: :users, only: [:show, :update] #-> url.com/profile
resources :listings, only: [:show] do
post :apply, on: :member #-> url.com/listings/:id/apply
end
resources :companies, controller: :users, only: [:show]
#app/controllers/users_controller.rb
class UsersController < ApplicationController
#show will automatically be loaded
def update
current_user.update profile_params
end
private
def profile_params
params.require(:user).permit(profile_attributes: [:name, :etc, :etc])
end
end
#app/views/users/show.html.erb
<%= form_for current_user do |f| %>
<%= f.fields_for :profile do |p|
<% if current_user.job_seeker? %>
<%= f.text_field :name, placeholder: "Your name" %>
<% elsif current_user.employer? %>
<%= f.text_field :name, placeholder: "Company name" %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
You'd then be able to use the following to check whether a user can create listings, or just view:
#app/controllers/listings_controller.rb
class ListingsController < ApplicationController
before_action :check_seeker, only: [:apply]
before_action :check_employer, only: [:new, :create, :destroy]
def new #-> employers
#listing = current_user.listings.new
end
def apply #-> job seekers
#listing = Listing.find params[:id]
#application = current_user.applications.new
#application.listing = #listing
redirect_to #listing, notice: "Application successful!" if #application.save
end
private
def check_seeker
redirect_to listings_path, notice: "Only Job Seekers Allowed" unless current_user.job_seeker?
end
def check_employer
redirect_to root_url, notice: "Only Employers Allowed" unless current_user.employer?
end
end
Hopefully this gives you the gist.
Devise
To get Devise working with your new column, you'll need to extend the Devise Sanitizer:
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :role
end
end
This will allow you to change the role field on signup:
#app/views/devise/registrations/new.html.erb
.....
<%= f.select :role, User.roles %>
Devise is good for authentication but for role and access control you may want to look into Rolify: https://github.com/RolifyCommunity/rolify.
This will allow you to keep a single user model and control access to different features with Role queries, ie:
unless current_user.has_role?(:admin)
redirect_to ...
else
render ...
end
For roles you can use CanCanCan gem and RoleModel gem with devise which will be better for user with different roles.
CanCanCan
Defining abilities using cancancan
Role model gem
Example for user roles with above gems..

Submit two forms at once with rails

Basically my idea is very simple - I want to create a new cart for each new user. The form itself is generated with scaffold and we're talking rails 4.0.1 here.
Is there a way to do that and if so - how? Maybe you can link me some live examples?
You do not need multiple forms to create multiple objects in Rails controller. Assuming that you have relationships like this:
class User < ActiveRecord::Base
has_many :carts #or has_one :cart
end
class Cart < ActiveRecord::Base
belongs_to :user
end
Then it's perfectly acceptable to do this:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new user_params
if #user.save
#user.carts.create # or #user.create_cart
redirect_to user_path
else
render action: :new
end
end
private
def user_params
params.require(:user).permit(...)
end
end
If the new user form happens to include some cart-specific details, then use fields_for to make them available in the form:
= form_for :user do |f|
... f.blah for user fields ...
= fields_for :cart do |cart_fld|
... cart_fld.blah for cart fields ...
and add cart_params to your controller.

Cancan Ruby Rails Hash Associated Conditionals

I am trying to prevent a user from accessing a profile that is not his/hers. My UserProfile model is linked to my profile model and has a column pertaining to the user_id. My thought was use cancan and "if a user.id does not match up with the associated user_id from the user_profile" then reject. I have tried to do this in so many different combinations and permutations and have only gotten cancan to reject if use the id field correlating to the UserProfile.
With the code below I can access
http://localhost:3000/users/8/profile and http://localhost:3000/users/6/profile
so the user has permission to see "use the show method of the UserProfile" of any user. So results are fetching properly but cancan is not limiting permissions based on the current User.id
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :registered
can :show, UserProfile, :user_id => user.id
end
end
end
class UserProfile < ActiveRecord::Base
attr_accessible :DOB, :user_id, :address_city, :address_country, :address_state, :address_street, :address_zip, :avatar, :first_name, :gender, :last_name, :position, :time_zone, :years_played
belongs_to :user
end
class User < ActiveRecord::Base
has_one :user_profile, :dependent => :destroy
end
Route File
get '/users/:user_id/profile/' => "user_profiles#show", :as => :user_profile
UserProfile Controller
class UserProfilesController < ApplicationController
load_and_authorize_resource
def show
##user = User.accessible_by(current_ability)
##user = User.find(params[:user_id])
##profile = #user.user_profile
#profile = UserProfile.where(:user_id => params[:user_id]).first
respond_to do |format|
format.html # show.html.erb
format.json { render :json => #profile }
end
end
end

Resources