I have a user model which uses Devise for authentication and also have an administrator model, which also uses Devise.
I want administrators to be able to edit users profile via administrators/users/{user.id}/edit, however I want this process to be done through Devise Controllers, therefore I tried to inherit from the Users::RegistrationsController as shown below:
class Administrators::UsersController < Users::RegistrationsController
before_action :set_user, only: [:show,:edit,:update,:destroy]
def index
#users=User.all
end
def show
end
def new
super
end
def update
#user.update(user_params)
redirect_to [:administrators,:users]
end
but I get the following error:
Could not find devise mapping for path "/administrators/users". This may happen for two reasons: 1) You forgot to wrap your route inside the scope block. For example: devise_scope :user do get "/some/route" => "some_devise_controller" end 2) You are testing a Devise controller bypassing the router. If so, you can explicitly tell Devise which mapping to use: #request.env["devise.mapping"] = Devise.mappings[:user]
I tried to change the routes but I still get the same error.
Could you please help me?
Inheriting from Devise::RegistrationsController may initially seem like a good idea from a code reuse standpoint but it really not a very good idea.
The intent of the controllers is very different - Devise::RegistrationsController partially deals with an un-authenicated user - and the Devise controllers are scary beasts due to the amount of flexibility built in Devise.
Instead you should just setup a plain old CRUD controller as the task at hand is not very complex compared to clobbering over half of Devise::RegistrationsController.
# config/routes.rb
namespace :administrators do
resources :users
end
# app/controllers/administrators/base_controller.rb
module Administrators
class AuthorizationError < StandardError; end
class BaseController
respond_to :html
before_action :authenticate_user!
# Replace with the lib of your choice such as Pundit or CanCanCan
before_action :authorize_user!
rescue_from AuthorizationError, with: :unauthorized
private
def authorize_user!
raise AuthorizationError and return unless current_user.admin?
end
def unauthorized
redirect_to new_session_path, alert: 'You are not authorized.'
end
end
end
class Administrators::UsersController < Administrators::BaseController
before_action :set_user, only: [:show, :edit, :update, :destroy]
def show
end
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.create(user_params)
respond_with(:administrators, #user)
end
def edit
end
def update
#user.update(user_params)
respond_with(:administrators, #user)
end
def destroy
#user.destroy
respond_with(:administrators, #user)
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
Instead you may want to focus on reusing the views through partials for example.
See:
ActionController::Responder
Pundit
CanCanCan
Related
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).
I'm following the Michael Hartl RoR tutorial, but implementing Rollify and Authority along the way. I've never used Authority before and I am wondering if the following before_action is appropriate for Authority use
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# Before filters
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
would it be "good programming practice" to put the def logged_in_user inside of the ApplicationAuthorizer class for future use?
Would it be "good programming practice" to put logged_in_user inside ApplicationAuthorizer
No.
There is a difference between Authentication and Authorization:
Authentication -- user logged in?
Authorization -- can user do this?
The difference is subtle but important - you'd expect authentication to happen before authorization, or at least independently.
A good analogy is authentication is when you get access to a secret party (password); authorization is which table you're able to sit at.
If you used one of the pre-rolled authentication systems (Devise or Sorcery), you'd have your authentication handled, providing you with such helpers as user_signed_in? etc.
To answer your question, your current pattern will suffice, considering you've rolled your own authentication.
If you were using Devise, you'd want to use the following:
#config/routes.rb
authenticate :user do
resource :profile, controller: :users, only: [:show, :update] #-> url.com/profile
end
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
#user = current_user
end
def update
#user = current_user.update update_params
end
end
--
What you're trying to do is evaluate the #user.id against current_user.id:
#app/models/user.rb
class User < ActiveRecord::Base
include Authority::UserAbilities
before_action :logged_in_user, only: [:edit, :update]
def edit
#user = User.find params[:id]
redirect_to root_path, notice: "Can't edit this user" unless current_user.can_edit?(#user)
end
def update
#user = User.find params[:id]
if current_user.can_update?(#user)
#user.update ...
else
# redirect
end
end
private
def logged_in_user
redirect_to login_url, error: "Please log in." unless logged_in?
end
end
# app/authorizers/user_authorizer.rb
class UserAuthorizer < ApplicationAuthorizer
def self.editable_by?(user)
user.id = self.id
end
def self.updatable_by?(user)
user.id = self.id
end
end
it might be a silly question but I am stuck with this for some time as I am new to rails.
I am basically using a custom registration controller to overwrite devise
class RegistrationsController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /Users
def index
#Users = User.all
end
# GET /Users/1
def show
end
# GET /Users/new
def new
#User = User.new
#course = Course.find_by id: params["course_id"]
end
# POST /Users
def create
#User = User.new(user_params)
if #User.save
redirect_to #User.paypal_url(registration_path(#User))
else
render :new
end
end
protect_from_forgery except: [:hook]
def hook
params.permit! # Permit all Paypal input params
status = params[:payment_status]
if status == "Completed"
#User = User.find params[:invoice]
#User.update_attributes notification_params: params, status: status, transaction_id: params[:txn_id], purchased_at: Time.now
end
render nothing: true
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#User = User.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:course_id, :name, :email, :password,:password_confirmation)
end
end
In my routes I have
devise_for :users ,:controllers => { :registrations => "registrations" }
So now I have
edit_user_registration_path GET /users/edit(.:format) registrations#edit
My question is how can I route only edit back to devise/registrations/edit or what can i add to my registrations controller so that I get something similar?
If you want to delegate the create action of the registrations controller to Devise, I recommend you to create a controller that inherits from the Devise one:
class RegistrationsController < Devise::RegistrationsController
def create
super #We call super because we don't want to override this action
end
def edit
#Custom code to override this action
end
end
Your route's configuration stays as it is, you just have to change your controller, you may also want to know that it's possible to ADD functionality to what devise already does, instead of override it:
def edit
super do |resource|
#Here you add what you'll do AFTER devise works
end
end
I'm trying to view my new action in my blogs controller, but I keep getting the following error message:
NameError in BlogsController#new
undefined local variable or method `authenticate_admin'
In my blogs controller, I want to restrict the new action to admins only (admins and users are two different models). I was able to get this to work in another model. If I'm not mistaken, helpers are open to all classes. I also tried to add the code from my admins helper to the blogs helper, but that didn't work.
Why can't my blogs controller use my authenticate_admin method?
Thanks for lookign :)
Here are relevant files:
blogs_controller.rb
class BlogsController < ApplicationController
before_filter :authenticate_admin, :only => [:new]
def new
#blog = Blog.new
#title = "New Article"
end
end
admins_helper.rb
def authenticate_admin
deny_admin_access unless admin_signed_in?
end
def deny_admin_access
redirect_to admin_login_url, :notice => "Please sign in as admin to access this page."
end
def admin_signed_in?
!current_admin.nil?
end
def current_admin
#current_admin ||= Admin.find(session[:admin_id]) if session[:admin_id]
end
In this case Helpers are accessible in your Views not in Controllers.
Solution is to move your methods from admins_helper.rb to ApplicationController and set them as helper_methods. You will be able to access them in your Controllers and Views.
Example:
class ApplicationController < ActionController::Base
# Helpers
helper_method :authenticate_admin
def authenticate_admin
deny_admin_access unless admin_signed_in?
end
end
Read documentation about helper_method:
http://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper_method
Given that it is well-documented how to use before_filter for a single user classification, I'm having trouble getting action-level protection for multiple user types. Let me explain:
I've got something like this...
class ApplicationController < ActionController::Base
class << self
attr_accessor :standard_actions
end
#standard_actions = [:index, :show, :new, :edit, :create, :update, :destroy]
def require_guardian
unless current_user and current_user.is_a?(Guardian)
store_location
redirect_to home_url
return false
end
end
def require_admin
unless current_user and current_user.is_a?(Administrator)
store_location
redirect_to register_url
return false
end
end
end
And in the GuardiansController I want to only allow the standard actions for Administrator but all other actions should require Guardian. So I tried this...
class GuardiansController < ApplicationController
before_filter :require_admin, :only => ApplicationController::standard_actions
before_filter :require_guardian, :except => ApplicationController::standard_actions
...
end
Which ends up doing a recursive redirection. There must be a better way?
OK, this is another case of not looking carefully and missing something. I inadvertently had setup the route to redirect the user in a recursive way. The above solution works just fine when you set the routes properly:
def require_guardian
unless current_user and current_user.is_a?(Guardian)
store_location
# this route (home_url) sent the user to another controller which ran a before_filter sending them back here again.
# redirect_to home_url
# So I changed it to a neutral route and it works great!
redirect_to register_url
return false
end
end