Devise: restricting page access using user attributes - ruby-on-rails

Relatively new to rails, I've got a simple web app using Devise for user authentication. One attribute is an :admin boolean, set nil for most users, and I will change to true manually in the console for the few users who will need to have administrative access.
My question is: How should I restrict access to a particular page to those who have admin access marked as true?
I've attempted some of that logic in my pages_controller, but it doesn't seem to redirect me as desired (referring to the user_list section):
class PagesController < ApplicationController
before_action :authenticate_user!, :except => [:welcome]
def welcome
#code removed for brevity's sake
end
def dashboard
#ditto
end
def user_list
unless
current_user.admin == true
redirect_to pages_dashboard_path
else
#users = Users.all
end
end
end
Any suggestions on my goal of redirecting or otherwise restricting access to my user_list page would be greatly appreciated.

in your controller you can do something like this
class PagesController < ApplicationController
...
def user_list
if current_user.admin == true
#users = Users.all
else
render :not_an_admin
end
end
end
You can not send them to the same page that they dont have access
You can choose to render a new view

In your user_list method, model name should be singular.
def user_list
unless
current_user.admin == true
redirect_to pages_dashboard_path
else
#users = User.all
end
end

Related

Authorize Users to perform various CRUD actions for each controller without using Pundit; Ruby on Rails

I am currently building a simple web app with Ruby on Rails that allows logged in users to perform CRUD actions to the User model. I would like to add a function where:
Users can select which actions they can perform per controller;
Ex: User A can perform actions a&b in controller A, whereas User B can only perform action B in controller A. These will be editable via the view.
Only authorized users will have access to editing authorization rights of other users. For example, if User A is authorized, then it can change what User B will be able to do, but User B, who is unauthorized, will not be able to change its own, or anyone's performable actions.
I already have my users controller set up with views and a model
class UsersController < ApplicationController
skip_before_action :already_logged_in?
skip_before_action :not_authorized, only: [:index, :show]
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
redirect_to users_path
else
render :new
end
end
def show
set_user
end
def edit
set_user
end
def update
if set_user.update(user_params)
redirect_to user_path(set_user)
else
render :edit
end
end
def destroy
if current_user.id == set_user.id
set_user.destroy
session[:user_id] = nil
redirect_to root_path
else
set_user.destroy
redirect_to users_path
end
end
private
def user_params
params.require(:user).permit(:email, :password)
end
def set_user
#user = User.find(params[:id])
end
end
My sessions controller:
class SessionsController < ApplicationController
skip_before_action :login?, except: [:destroy]
skip_before_action :already_logged_in?, only: [:destroy]
skip_before_action :not_authorized
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to user_path(user.id), notice: 'You are now successfully logged in.'
else
flash.now[:alert] = 'Email or Password is Invalid'
render :new
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: 'You have successfully logged out'
end
end
The login/logout function works, no problem there.
I started off by implementing a not_authorized method in the main application controller which by default prevents users from accessing the respective actions if the user role is not equal to 1.
def not_authorized
return if current_user.nil?
redirect_to users_path, notice: 'Not Authorized' unless current_user.role == 1
end
the problem is that I would like to make this editable. So users with role = 1 are able to edit each user's access authorization, if that makes sense.
How would I go about developing this further? I also do not want to use gems, as the sole purpose of this is for me to learn.
Any insights are appreciated. Thank you!
The basics of an authorization system is an exception class:
# app/errors/authorization_error.rb
class AuthorizationError < StandardError; end
And a rescue which will catch when your application raises the error:
class ApplicationController < ActionController::Base
rescue_from 'AuthorizationError', with: :deny_access
private
def deny_access
# see https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses
redirect_to '/somewhere', status: :forbidden
end
end
This avoids repeating the logic all over your controllers while you can still override the deny_access method in subclasses to customize it.
You would then perform authorization checks in your controllers:
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
def create
#thing = current_user.things.new(thing_params)
if #thing.save
redirect_to :thing
else
render :new
end
end
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless #thing.user == current_user || current_user.admin?
end
end
In this pretty typical scenario anybody can create a Thing, but the users can only edit things they have created unless they are admins. "Inlining" everything like this into your controllers can quickly become an unwieldy mess through as the level of complexity grows - which is why gems such as Pundit and CanCanCan extract this out into a separate layer.
Creating a system where the permissions are editable by users of the application is several degrees of magnitude harder to both conceptualize and implement and is really beyond what you should be attempting if you are new to authorization (or Rails). You would need to create a separate table to hold the permissions:
class User < ApplicationRecord
has_many :privileges
end
class Privilege < ApplicationRecord
belongs_to :thing
belongs_to :user
end
class ThingsController
before_action :authorize!, only: [:update, :edit, :destroy]
# ...
private
def authorize!
#thing.find(params[:id])
raise AuthorizationError unless owner? || admin? || privileged?
end
def owner?
#thing.user == current_user
end
def admin?
current_user.admin?
end
def privileged?
current_user.privileges.where(
thing: #thing,
name: params[:action]
)
end
end
This is really a rudimentary Role-based access control system (RBAC).

How to restrict access to different pages in Rails?

I have controller Users:
class UsersController < ApplicationController
......
def show
#user = User.find(params[:id])
end
def index
#users = User.paginate(page: params[:page], per_page: 25)
end
......
end
Now users profiles are at /users/1, /users/2, etc. and list of users is at /users/.
I want to give special access:
user can see only own profile
admin can see the list of users and any profile
How can I restrict access this way?
Assuming you have a current_user defined and your User class has an admin attribute you can do the following:
class UsersController < ApplicationController
......
def show
#user = User.find(params[:id])
if current_user.admin || #user == current_user
# render the show screen
else
# redirect to wherever
end
end
def index
if current_user.admin
#users = User.paginate(page: params[:page], per_page: 25)
# render the index screen
else
# redirect to wherever
end
end
......
end
Or you could just use one of the plenty of authorization gems out there, like cancancan or pundit.
You should use ACL libraries like cancancan or pundit or from ruby-toolbox.com
I would probably handle this by having two different endpoints, something like /profile and /admin/users/1. Then you have different controllers for them:
UserProfileController < ApplicationController
def show
#user = current_user
end
end
and:
class Admin::UsersController < AdminController
def show
#user = User.find(params[:id])
render 'user_profile/show' # or another template if you like
end
end
class AdminController < ApplicationController
before_action :ensure_admin
def ensure_admin
if !current_user.admin?
raise ActionController::RoutingError, 'Not Found'
end
end
end
Considering your url user/1/ you grab the param id and compare it to the current user ID in a hook :
before_action :auth_user
private
def auth_user
unless params[:id].to_s == current_user.id.to_s
redirect_to root_path
end
Regarding the admin you probably have a dedicated namespace, with even more thorough checks, where you can see user profiles.

Authorise user to edit a particular field using Pundit in Rails

I'm running Pundit in my Rails app for authorisation. I seem to be getting the hang of it all but want to know how to restrict the edit or update actions to a certain field.
For example, a user can edit their user.first_name, user.mobile or user.birthday etc but can't edit their user.role. Essentially my logic is, let the user edit anything that's cosmetic but not if it is functional.
These fields should only be able to be edited by a user who has a 'super_admin' role (which I have setup on the user.rb with methods such as the below).
def super_admin?
role == "super admin"
end
def account?
role == "account"
end
def segment?
role == "segment"
end
def sales?
role == "sale"
end
def regional?
role == "regional"
end
def national?
role == "national"
end
def global?
role == "global"
end
I pretty much have a clean slate user_policy.rb file where the update and edit actions are the default
def update?
false
end
def edit?
update?
end
Maybe I am thinking entirely wrong about this and should just wrap a user.super_admin? if statement around the role field on the user show page but this feels wrong if I am only using that tactic for security.
Use Pundit's permitted_attributes helper which is described on the gem's README page: https://github.com/elabs/pundit
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
[:tag_list]
end
end
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
#post = Post.find(params[:id])
if #post.update_attributes(post_params)
redirect_to #post
else
render :edit
end
end
private
def post_params
params.require(:post).permit(policy(#post).permitted_attributes)
end
end
In your views, you can limit what users can see based on their role.
User View
- if current_user.super_admin?
= f.select(:role, User.roles.keys.map {|role| [role.titleize.role]})
- else
= user.role
And in the policy you can call the role of the user to make sure they are able to edit.
class UserPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#user = model
end
def edit?
#current_user.super_admin || #current_user == #user
end
end

Check if current_user is the owner of a resource and allow edit/delete actions

Example:
User A (id=10) has created a photo resource
photo: (id: 1 user_id = 10, url: "http://...")
Now, if User B (id=20) go to this url: /photos/1/edit it can edit photo of user A!!!
Rails+Devise provides something for this by default? It seems it's a very common issue
I just need to allow that any user can edit/delete ONLY resource it has created (where current_user == resource.user)
Using: Rails 4, Devise
Update:
I think CanCan it's something too advanced. I don't need roles or restrict some actions to certain users
In your PhotosController:
before_filter :require_permission, only: :edit
def require_permission
if current_user != Photo.find(params[:id]).user
redirect_to root_path
#Or do something else here
end
end
You can make use of Rails' associations and write it like this:
def edit
#photo = current_user.photos.find(params[:id])
# ... do everything else
end
This will only find a record when the photo with the supplied ID belongs to the current user. If it doesn't, Rails will raise a ActiveRecord::RecordNotFound exception.
Of course, I'm assuming the current_user method is available and your User model contains the statement has_many :photos.
Check this railscasts,
http://railscasts.com/episodes/192-authorization-with-cancan
Complications you will run into,
When you want cancan authorization on User Model that Devise gem is using for authentication
When you want to store your Roles in the Database
When you want to assign Permissions to the Roles as an Admin from the webUI
and more ..
Please comment if you want any of those features, I will be happy to help, because I recently did them with great help from others and its always amazing to pass it on.
A sample Ability for your resources can be like as follows,
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest users
send(user.role.name)
if user.role.blank?
can :read, User #for guest without roles
end
end
def man
can :manage, Photo
end
def boy
can :read, Photo
end
def kid
can :read, Article
end
end
I captured the exception from within a before_filter action:
before_action :set_photo, only: [:edit, :update, :destroy]
def set_photo
#photo = current_user.photos.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to(root_url, :notice => 'Record not found')
end
Hope this helps someone. I'm using Rails 4 and Ruby 2.
So you are using gem devise.
This gem provides the current_user for the currently logged in user.
In your PhotosController#edit method. I'd do something like below.
def edit
#photo = Photo.find(params[:id])
redirect_to root_path, notice: 'Thou Shalt Nought duuu dat :(' unless current_user.id == #photo.user_id
...
end
This method is cheaper because you already have 2 objects to compare instead of running a query in the comparison.
The simplest would be to to modify routes.rb.
Assign photos to live in the current_user path.
For example,
devise_for :users
resources 'users' do
resources 'photos'
end
cancan is difficult and complicate
i have coding is_onwer method
it's very simple, easy
https://gist.github.com/x1wins/0d3f0058270cef37b2d3f25a56a3745d
application controller
def is_owner user_id
unless user_id == current_user.id
render json: nil, status: :forbidden
return
end
end
def is_owner_object data
if data.nil? or data.user_id.nil?
return render status: :not_found
else
is_owner data.user_id
end
end
your controller
before_action only: [:edit, :update, :destroy] do
is_owner_object #article ##your object
end
If CanCan is too advanced, you should loon into checking the id of the accessor in the controller using...
if #user.id == #photo.user_id
# edit photo details
else
redirect_to root_path, notice: "You! Shall! Not! Edit!"
...or something like that
Write another before_filter in application_controller:
before_filter :has_permission?
has_permission?
controllers=["articles", "photos", "..."]
actions=["edit", "destroy", "..."]
id = params[:id] if (controllers.include?(params[:controller] && actions.include?(params[:action]) end
if id && (current_user.id==(params[:controller][0...1].capitalize!+params[:controller].singularize[1...-1] + ".find(#{id}).user_id").send)
return true
else
redirect_to root_url, :notice=>"no permission for this action"
end
helper_method :has_permission?
And you can use it in views, not to show users link they can't follow.
Some kind of this, of course you need to modify it to suit your needs.

Rails redirect based on user type

I'm learning Rails by building a shop application and I'm having a bit of trouble with redirects. I have 3 roles in the application:
Buyer
Seller
Administrator
Depending on which type they are logged in as then I would like to redirect to a different page/action but still show the same URL for each (http://.../my-account).
I don't like having to render partials in the same view, it just seems messy, is there another way to achieve this?
The only way I can think of is to have multiple actions (e.g. buyer, seller, administrator) in the accounts controller but that means the paths will look like http://.../my-account/buyer or http://.../my-account/seller etc.
Many thanks,
Roger
I've put my code below:
models/user.rb
class User < ActiveRecord::Base
def buyer?
return type == 'buyer'
end
def seller?
return type == 'seller'
end
def administrator?
return type == 'administrator'
end
...
end
controllers/accounts_controller.rb
class AccountsController < ApplicationController
def show
end
end
controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
#user_session = UserSession.new
end
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
if session[:return_to].nil?
# I'm not sure how to handle this part if I want the URL to be the same for each.
redirect_to(account_path)
else
redirect_to(session[:return_to])
end
else
#user_session.errors.clear # Give as little feedback as possible to improve security.
flash[:notice] = 'We didn\'t recognise the email address or password you entered, please try again.'
render(:action => :new)
end
end
def destroy
current_user_session.destroy
current_basket.destroy
redirect_to(root_url, :notice => 'Sign out successful!')
end
end
config/routes.rb
match 'my-account' => 'accounts#show'
Many thanks,
Roger
In UserSessionsController#create (i.e.: the login method) you could continue to redirect to the account path (assuming that goes to AccountsController#show) and then render different views according to the role. I.e.: something like this:
class AccountsController < ApplicationController
def show
if current_user.buyer?
render 'accounts/buyer'
elsif current_user.seller?
render 'accounts/seller'
elsif current_user.administrator?
render 'accounts/administrator
end
end
end
Better yet, you could do this by convention...
class AccountsController < ApplicationController
def show
render "accounts/#{current_user.type}"
end
end
If I understand you question correctly, then the solution is simple.
You can just call the method you want inside your controller. I do this in my project:
def create
create_or_update
end
def update
create_or_update
end
def create_or_update
...
end
In your case it should be:
def action
if administrator? then
admin_action
elsif buyer? then
buyer_action
elseif seller? then
seller_action
else
some_error_action
end
end
You should probably explicitly call "render" with an action name in each of those actions, though.

Resources