Override Devise controller -- only user with admin role to log in? - ruby-on-rails

How do you override the Devise controller to only allow 'admins' to log in?
This is what I came up with:
class SessionsController < Devise::SessionsController
def create
if current_user.admin?
# tell the user "you can't do that"
else
super
end
end
end
but the user was able to log in (probably because 'current_admin' is not defined yet?). Here is the original devise controller action:
class Devise::SessionsController < DeviseController
prepend_before_filter :require_no_authentication, only: [:new, :create]
prepend_before_filter :allow_params_authentication!, only: :create
prepend_before_filter :verify_signed_out_user, only: :destroy
prepend_before_filter only: [:create, :destroy] { request.env["devise.skip_timeout"] = true }
...
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_flashing_format?
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
...
end
Edit: I don't think I should change the session controller, I think I should add a strategy to Warden. I tried this and it still logs in non admin users:
config/initializers/custom_warden_strategies.rb:
Warden::Strategies.add(:admin_only) do
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
encrypted = false
if validate(resource) { encrypted = true; resource.valid_password?(password) }
if resource.admin?
remember_me(resource)
resource.after_database_authentication
success!(resource)
end
end
mapping.to.new.password = password if !encrypted && Devise.paranoid
fail(:not_found_in_database) unless resource
end
end
config\initializers\devise.rb
config.warden do |manager|
manager.default_strategies.unshift :admin_only
end

Give this a try:
class SessionsController < Devise::SessionsController
def create
super do
if !resource.admin?
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
set_flash_message :notice, :signed_out if signed_out && is_flashing_format?
respond_to_on_destroy
end
end
end
end

I found a solution, but it fails with my test suite (works when I manually test it though).
config/initializers/admin_only_initializer.rb
require 'devise/strategies/authenticatable'
module Devise
module Strategies
# Default strategy for signing in a user, based on their email and password in the database.
class AdminOnly < Authenticatable
def authenticate!
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
encrypted = false
if validate(resource){ encrypted = true; resource.valid_password?(password) }
if resource.admin?
success!(resource)
else
fail!(:not_permitted)
end
end
mapping.to.new.password = password if !encrypted && Devise.paranoid
fail(:not_found_in_database) unless resource
end
end
end
end
Warden::Strategies.add(:admin_only, Devise::Strategies::AdminOnly)
config/initializers/devise.rb
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :admin_only
end
and the I18n string (config/locales/devise.en.yml):
en:
devise:
failure:
not_permitted: "You are not permitted to complete this action."

Why prevent non-admins from logging in and not just block some actions for non-admin users ? How do you make the difference between an admin and a simple user then ?
Remember, the one true security principle is DENY then ALLOW. So to make sure you application remains safe when you keep adding stuff (AGILE development for example), I suggest the following approach
application_controller.rb
class ApplicationController
before_action :authenticate_user!
# Security policy deny then access
before_filter :access_denied
# Actually I have refactorised below code in a separate security.rb module that I include in ApplicationController, but as you wish
def access_denied
if #denied and not #authorized
flash[:alert] = 'Unauthorized access'
flash[:info] = "Authorized entities : #{#authorized_entities.join(', ')}" if #authorized_entities
flash[:warning] = "Restricted to entities : #{#restricted_entities.join(', ')}" if #restricted_entities
render 'static_pages/home', :status => :unauthorized and return
false
end
end
def allow_access_to_administrators
(#authorized_entities ||= []) << "Administrators"
#authorized = true if administrateur_logged_in?
end
def administrateur_signed_in?
user_signed_in? and current_user.administrator? # Or whatever method you use to authenticate admins
end
end
Note that I use both #authorized and #denied.
I use #authorized generally for a class of users (like admins), whereas I set #denied if, for a class of users, I want to restrict to a subset.
Then I use
your_controller_reserved_for_admins.rb
prepend_before_filter :allow_access_to_administrators

Related

how to authenticate the user with warden/devise without password?

In my app during login, user will provide only email, password is not required.I want to authenticate it using devise/warden.
class Users::SessionsController < Devise::SessionsController
include ExpireExistingToken
def new
super
end
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
resource.update(language: Constant::SUPPORTED_LANGUAGES.key(params['locale'])) if params['locale'].present?
resource.send_otp(force_expire: true) if resource.email_verified?
respond_with resource, location: after_sign_in_path_for(resource)
flash.delete(:notice)
end
def destroy
session.delete(:is_otp_verified)
super
end
end
# frozen_string_literal: true
# Responsible for token expiration
module ExpireExistingToken
extend ActiveSupport::Concern
included do
before_action :remove_existing_token, only: :new
end
protected
def remove_existing_token
uuid = session[:uuid]
usr = User.find_by(uuid: uuid) if uuid
return unless usr
usr.send(:clear_reset_password_token)
usr.save
session.delete(:uuid)
end
end
I tried to override valid_password? in User model, but it didn't work. Please help me out on this.
You can try to override Devise password required validation method and return false during save. If you want to disable it only for resource or functionality specific, you need to send virtual attribute from form during save.
class User < ActiveRecord::Base
attr_accessor :skip_validation
protected
def password_required?
return false if skip_validation
super
end
end

Rails 5 / Devise - delete users as admin

I'm using devise to manage authentication. I have a User model and Admin model. I want to be able to allow both users and admins soft delete user accounts.
I have implemented the soft delete for users and everything works well, however, adding functionality for admins results in a 401 unauthorized and a redirect to the user sign in page. I'm not exactly sure how to get around this.
So far I have:
config/routes.rb
...
devise_for :users
devise_scope :user do
resources :users, only: [:destroy], controller: 'members/registrations', as: :user_registration do
get 'cancel'
end
end
...
controllers/members/registrations_controller.rb
class Members::RegistrationsController < Devise::RegistrationsController
def destroy
#user = User.find(params[:id])
not_authorized unless authorized?
#user.soft_delete
user_post_destroy if is_current_user?
end
private
def authorized?
if signed_in?
is_current_user?
else
session[:session_id] == #user.author_session_token
end
end
def not_authorized
flash[:error] = t('errors.messages.not_authorized')
flash.keep
redirect_back(fallback_location: root_path)
end
def user_post_destroy
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message :notice, :destroyed
yield resource if block_given?
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
def is_current_user?
#user == current_user
end
end
models/user.rb
...
def soft_delete
update_attribute(:deleted_at, Time.current)
end
def active_for_authentication?
super && !deleted_at
end
def inactive_message
!deleted_at ? super : :deleted_account
end
...

Appropriate use of Authority in rails app

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

pundit_user: undefined method `current_user' for #<User:0x007fcefbc2b150>

I have two layouts Admin and Domain. And I don't need any extra configuration in Admin layout. but if user tries to access Domain layout they must be in their valid domain.
This means that, I need to customize all of my Domain policy to include both current_user as well as current_domain. I found this can be done with UserContext and pundit_user... so here is what I have done:
application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def pundit_user
UserContext.new(current_user, current_domain)
end
def after_sign_out_path_for(resource)
root_path
end
def current_domain
#current_domain ||= Domain.where(name: requested_domain).first
end
helper_method :current_domain
private
def requested_domain
return request.env["SERVER_NAME"]
end
def user_not_authorized
# reset_session
flash[:alert] = "You are not authorized to perform this action"
redirect_to(request.referrer || root_path)
end
end
Note that, when I access Admin layout, current_domain will be nil and if I visit any routes of Domain layout, then current_domain will set to currently accessing domain.
user_context.rb
class UserContext
attr_reader :current_user, :current_domain
def initialize(current_user, current_domain)
#current_user = current_user
#current_domain = current_domain
end
end
PROBLEM
Suppose I have this policy:
user_policy.rb
class UserPolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def index?
binding.pry # debugging
current_user.admin? ||
current_user.domain == current_domain
end
private
def current_user
# return user.is_a?(User) ? user : user.current_user
user.current_user
end
def current_domain
# return user.is_a?(User) ? nil : user.current_domain
user.current_domain
end
end
when application runs current_user and current_domain must available in UserPolicy as per documentation(https://github.com/elabs/pundit#additional-context).
But I am getting
undefined method `current_user' for #<User:0x007fcefbc2b150>
That means, still I have user object in it, not user.current_user and user.current_domain
Please let me know, if you need further description. What am I missing here?
It was my own dumb mistake.
PROBLEM
I had a before_filter call in domain/base_controller.rb something like:
class Domain::BaseController < ApplicationController
before_action :authenticate_user!
before_action :domain_exists?
before_action :verify_domain!
private
def verify_domain!
# PROBLEM: this line was updating pundit_user again to user object
raise Pundit::NotAuthorizedError unless DomainConsolePolicy.new(current_user, current_domain).authorized?
end
def domain_exists?
if current_domain.blank?
redirect_to root_path, alert: 'Domain that you provided is not valid or is permanently removed!'
end
end
end
SOLUTION:
I have used headless policy for this because now I have both current_user and current_domain set with pundit_user in application_controller
domain/base_controller.rb
class Domain::BaseController < ApplicationController
before_action :authenticate_user!
before_action :domain_exists?
before_action :verify_domain!
private
def verify_domain!
# SOLUTION
authorize :domain_console, :has_access?
end
def domain_exists?
if current_domain.blank?
redirect_to root_path, alert: 'Domain that you provided is not valid or is permanently removed!'
end
end
end
policy/domain_console_policy.rb
class DomainConsolePolicy < Struct.new(:user, :domain_console)
def has_access?
user.current_user.admin? ||
user.current_user.domain_id == user.current_domain.id
end
end
Thanks

Rails: Basic Authentication with Authlogic

I'm using Authlogic and I would like to implement Basic HTTP Authentication in my controller so I could define which action requires authentication.
I know how to do Basic HTTP Authentication authenticate_or_request_with_http_basic an before_filter, but I would like to here from other how to implement it with Authlogic plugin.
class ItemsController < ApplicationController
before_filter :authenticate , :only => [:index, :create]
...
end
I've had success with the following:
Define a filter method in application_controller.rb
def require_http_auth_user
authenticate_or_request_with_http_basic do |username, password|
if user = User.find_by_login(username)
user.valid_password?(password)
else
false
end
end
end
Then in your controller you can do the following:
before_filter : require_http_auth_user
You can then use:
http://username:password#yoursite.com (i.e. http basic authentication)
Hope this helps.
Here is a great screencast that explains, step-by-step, how to use authlogic in your rails project.
Once authlogic is set up, define the following useful authentication-related helper methods in your Application Controller.
def current_user_session
return #current_user_session if defined?(#current_user_session)
#current_user_session = UserSession.find
end
def current_user
return #current_user if defined?(#current_user)
#current_user = current_user_session && current_user_session.record
end
def require_user
unless current_user
store_location
flash[:notice] = "You must be logged in to access this page"
redirect_to new_user_session_url
return false
end
end
def require_no_user
if current_user
store_location
flash[:notice] = "You must be logged out to access this page"
redirect_to root_url
return false
end
end
Once those methods are defined, you can specify actions that require the user to be logged in:
before_filter :require_user, :only => [:new, :edit]

Resources