I am rolling R Bates authentication from scratch from here, and I'm wanting to put the call to the authorize method in the application controller. Basically I want the entire app locked down. Here is the app controller...
class ApplicationController < ActionController::Base
before_filter :authorize
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
def authorize
redirect_to login_url, alert: "Not authorized" if current_user.nil?
end
end
But the probably is I'm getting an infinite loop in my URL call. How should I hand this?
sessions controller
class SessionsController < ApplicationController
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 root_url, notice: "Logged in!"
else
flash.now.alert = "Email or password is invalid"
render "new"
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, notice: "Logged out!"
end
end
You can skip before_filter (like in #vee answer) for some actions of SessionsController:
skip_before_filter :authorize, only: [:new, :create]
Alternatively you can modify authorize method to avoid redirects in some cases:
def authorize
return if skip_authorization?("#{controller_name}##{action_name}")
redirect_to login_url, alert: "Not authorized" if current_user.nil?
end
def skip_authorization?(location)
%w(sessions#new sessions#create).include?(location)
end
The loop is because of the redirect_to login_url....
You should skip the authorize filter in controller that has the login action defined as:
class SessionsController < ApplicationController
skip_before_filter :authorize, only: :login
def login
...
end
...
end
Or to skip authorize filter for all actions, use skip_before_filter :authenticate without the only option.
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).
My user model has birthday and I'm trying to redirect to the edit page if it is blank after signing in with Facebook. I tried overriding the after_sign_in_path for resource but keep getting this error:
Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".
My Application controller:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def after_sign_in_path_for(resource)
stored_location_for(resource) ||
if resource.birthday.blank?
redirect_to edit_user_path(resource)
end
super
end
end
and my Omniauth controller:
class OmniauthCallbacksController < ApplicationController
skip_before_filter :authenticate_user!
def provides_callback_for
user = User.from_omniauth(env["omniauth.auth"], current_user)
if user.persisted?
flash[:notice] = "You have signed in!"
sign_in_and_redirect(user)
else
session['devise.user_attributed'] = user.attributes
redirect_to new_user_registration_url
end
end
def failure
flash[:notice] = "Something went wrong!"
redirect_to root_path
end
alias_method :facebook, :provides_callback_for
end
This should do the trick:
def after_sign_in_path_for(resource)
if resource.birthday.blank?
edit_user_registration_url
else
super
end
end
I have a jobs model and a link to create a new job. I would like to force a user to sign in before visiting the new job form. I have set up a before_action that forces sign in.
application_controller.rb
helper_method :current_user
helper_method :require_signin!
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def require_signin!
if current_user.nil?
redirect_to signin_path
end
end
jobs_controller.rb
before_action :require_signin!, only: [:new]
routes.rb
get '/auth/twitter' => 'sessions#new', :as => :signin
sessions_controller.rb
class SessionsController < ApplicationController
def create
auth = request.env["omniauth.auth"]
user = User.from_omniauth(auth)
session[:user_id] = user.id
redirect_to user, :notice => "Signed in!"
end
end
Current behavior
When a user, who is not signed in, clicks the 'jobs/new' link, the chain of events is jobs#new -> login -> user (default redirect after signing in). User then must navigate back to jobs#new
Desired behavior
When a user, who is not signed in, clicks the 'jobs/new' link, the chain of events is jobs#new -> login -> jobs#new.
I know the before_action is intercepting the original action, but I would like to complete the original action after signing in. Help?
To implement this, you can save the original route in session before redirecting to sign_in route, like:
class JobsController
before_action :save_original_path, only: [:new]
before_action :require_signin!, only: [:new]
private
def save_original_path
session[:return_to] = new_job_path
end
end
and
class SessionsController
def create
...
redirect_to (session[:return_to] || user), :notice => "Signed in!"
end
end
class UserSessionsController < ApplicationController
skip_before_action :require_login, except: [:destroy]
def new
...
end
def create
...
end
def destroy
logout
redirect_to signin_path , flash: { info: 'Bye!' }
end
end
class ApplicationController < ActionController::Base
before_action :require_login
private
def not_authenticated
redirect_to signin_path, flash: { danger: "ALARM!" }
end
end
After I logout, I'm redirected to sign in page with flash message "ALARM".
After that, when I log in again I'm redirected to sign in page with flash message "Bye!"
Please, help!
Ok, this was some kind of sorcery bug. You just need to skip require_login before destroy session too
I'm using Rails 3.2 and Authlogic. I have the following code:
class ApplicationController < ActionController::Base
private
def store_location
session[:return_to] = request.url
end
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
end
class UserSessionsController < ApplicationController
before_filter :require_no_user, :only => [:new, :create]
before_filter :require_user, :only => :destroy
def new
#user_session = UserSession.new
#header_title = "Login"
end
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
flash[:success] = "Login successful!"
redirect_back_or_default root_url
else
render 'new'
end
end
def destroy
current_user_session.destroy
flash[:success] = "Logout successful!"
redirect_back_or_default root_url
end
end
This code is quite generic. When we use the before_filter:
before_filter :require_user, :only => [:new, :edit, :update, :create]
It will automatically store_location and redirect us back to the proper page. However, how do I do this:
I'm in posts/1 which doesn't require_user.
I click the login link on my top navigation bar.
It shows the login page.
Once login, I will be redirected back to posts/1 instead of the root_url.
Place a direct call to store_location in the sessions controller new action.
# user_sessions_controller.rb
def new
store_location if session[:return_to].blank?
#user_session = UserSession.new
#header_title = "Login"
end
This will first check for an existing return_to pair in the sessions hash. You don't want to overwrite it in case, for example, a user is redirected to the new action because of a bad password.
This will also skip store_location if it was already called from require_user.
After a successful redirect, you have to delete the return_to pair from the sessions hash; setting it to nil is not enough:
# application_controller.rb
def redirect_back_or_default(default)
redirect_to(session.delete(:return_to) || default)
end
I added a store_referrer_location to make it work:
# application_controller.rb
class ApplicationController < ActionController::Base
private
def store_referrer_location
session[:return_to] = request.referrer
end
end
# user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
store_referrer_location if session[:return_to].blank?
#user_session = UserSession.new
#header_title = "Login"
end
...
def destroy
store_referrer_location if session[:return_to].blank?
current_user_session.destroy
flash[:success] = "Logout successful!"
redirect_back_or_default root_url
end
end