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
Related
This is my usercontroller.rb
class UsersController < ApplicationController
before_action :user_logged_in, only: [:show,:edit ,:update, :index, :destroy]
before_action :check_admin, only: [:index]
before_action :check_correct_user, only: [:edit ,:update]
def edit
#user=User.find(params[:id])
end
This is update method
update
def update
#user=User.find(params[:id])
if #user.update(user_req)
flash[:success]="Profile Updated"
redirect_to #user
else
render 'edit'
end
this is params
private
def user_req
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
i created another method for check_admin
def check_correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless #user == current_user ||
current_user.admin?
end
end
I am not sure if I understand our question correctly. Do you want users to be able to update their own name, email, and password? And admins should be able to update other users too, but only their names?
Then I would do it like this:
def user_req
if current_user.admin? && #user != current_user
params.require(:user).permit(:name)
else
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
The condition is a bit more complex than expected because an admin user still needs to be able to update their own email and password.
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).
Root page of my app is post's index page
Rails.application.routes.draw do
root 'posts#index'
resources :users
resources :posts
resource :sessions, only: [:new, :create, :destroy]
resources :passwords, only: [:new, :create, :edit, :update]
end
My uses cookies to store user sessions. At the time of login, if the user checks the remember me checkbox, its session is remembered and he is able to access the app after closing the browser without needing to log in again.
Step I follow:
1. Run rails s on the terminal.
Type localhost:3000 on the browsers address bar
The app opens up,I land on posts#index page as it is root path my app follows.
I click on the login link, I fill email, password & check remember me checkbox
I land on user's profile page i.e., users#show page
Terminal till this point
Now I close the browsers window. (I do not log out.)(cookies should rememeber my details)
I again open the browser. type localhost:3000 on browser's addreess bar.
I land 'posts#index' page. But I want to not on the user's profile i.e, users#show page as my details are stored in cookies
But I want to land on the user's profile page.
On the application controller, if do...
before_action :set_cache_buster,:redirect_if_logged_in
def redirect_if_logged_in
redirect_to user_path(current_user) if logged_in?
end
And login if says redirect too many times
Application Controller:
# frozen_string_literal: true
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user,:logged_in?, :logged_in_user,:current_user
before_action :set_cache_buster
def log_in(user)
session[:user_id] = user.id
end
def current_user
#current_user ||= User.where("auth_token =?", cookies[:auth_token]).first if cookies[:auth_token]
end
def logged_in?
!current_user.nil?
end
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to new_sessions_path
end
end
def current_user?(user)
user == current_user
end
private
def set_cache_buster
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end
end
User Contoller
# frozen_string_literal: true
require 'resolv-replace'
class UsersController < ApplicationController
before_action :logged_in_user, only: [:show, :edit, :update]
before_action :correct_user, only: [:show, :edit, :update]
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(set_params)
if #user.save
UserNotifierMailer.send_signup_email(#user).deliver
flash[:success] ="Success"
redirect_to new_sessions_path
else
render 'new'
end
end
def show
#user = User.find(params[:id])
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update(update_params)
redirect_to #user
else
render 'edit'
end
end
private
def set_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def update_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def correct_user
#user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to(#current_user) unless current_user?(#user)
end
end
After I type localhost:3000 redirecting to 'post#index' is fine. but I add a conditional redirect. If cookies are not empty redirect to some other path.
I hope i am able to explain the problem well at least now.
You have redirect_if_logged_in method called each time you are being redirected as your UsersController inherits from ApplicationController. As I understood you just need to move this code to PostsController
Try doing this, add before_action
before_action :check_logged_in
def check_logged_in
unless logged_in?
flash[:danger] = "Please log in."
redirect_to new_sessions_path
else
redirect_to user_path(current_user) if cookies[:auth_token]
end
end
You don't need logged_in_user as helper method after adding this
Hope that helps!
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
So I am having some trouble with my rails app and I think I went a little out of my own depth. I am creating a simple alumni application and I want users to be able to join organizations. For some reason in my new join page I get the error "Couldn't find User without an ID". I want to know why the ID isn't passing in, which would imply signed_in? = false. I don't know why everything worked find when I created other additions to my users controller but here it refuses to take on the logged in user id. I feel like I am missing something simple, let me know if updates are necessary!
Here is the relevant information in my Users controller:
class UsersController < ApplicationController
before_action :signed_in_user, only: [:edit, :update, :index, :show, :join]
before_action :correct_user, only: [:edit, :update, :join]
before_action :admin_user, only: :destroy
def join
end
def show
#user = User.find(params[:id])
#organization = #user.organization
end
def create
#user = User.new(user_params)
if #user.save
sign_in #user
flash[:success] = "Welcome to the Sample App!"
redirect_to #user
else
render 'new'
end
end
...
private
def user_params
params.require(:user).permit(:name,:email, :password, :password_confirmation,:organization_id)
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end
I included the def create method because I tried editing it to redirect users to the join page right after login but then I came across this error and I thought that was the problem so i switched it back. I guess it wasnt.... NOTE: I am basing a lot of this app off of the Hartl tutorial if that is helpful.
You should have a Memberships controller and model with a belongs_to :user (has_many :memberships for User & Organization), instead of defining a join method in the Users controller. The controller should be responsible for adding/deleting organization user-memberships. From that controller you fetch the user info by #user = User.find(:id) and don't forget to properly set the route file for nested resources.
resources :users do
resources :memberships
end
Also note that your join method doesn't create any instance variables for the view (#user). It looks like it properly goes through the signed_in_user action but nothing is instantiated in the join method.
Change line 3 to:
before_action :correct_user, only: [:edit, :update]
If the purpose of "join" is to create a user, then there should not be a user yet. However if your purpose for "join" is to get the current user you should add this to your join method:
#user = current_user
What about if you changed it to this:
def show
#users = User.all
#user = #users.find(params[:id])
#organization = #user.organization
end