Rails Basic Authorization (after Basic Authentication)

Hi! The hobby developer is back. I've been doing RoR since version 0.9. I have a few semi-private apps deployed and try to keep up with versions. When DHH released the Basic Authentication Scaffold I played with it and got it working. It did bother me that there is stuff in there that I have no idea on what it does. Like: def start_new_session_for(user) user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| Current.session = session cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } end end Have no idea what .tap does. Anyhow it works! Now on to a new challenge - Authorization. I only have one app that has some form of role based Authorization. It's a golf group management app. It just kept track of groups, players, points, quotas etc. Kinda like a Handicap. I have several groups that use it. Each group has a user/manager that can do anything and a few that can do most things - form team - score teams etc. It turned out that they just share the User account and have two or three players that can do anything! I think I tried Devise but didn't like the pain. Later tried CanCan and then CanCanCan. It was ok, but then I rolled my own. Didn't have much to do so I spent the last week rolling another version! I forgot to say that I just turned 80 and don't have much to do. Just though I'd share it. Much like CanCanCan it has an Ability scheme that defines who can do what. In mine it's just a Constant wrapped in a class. class Can CRUD = { super:{ group:'1111', user:'1111', player:'1111', game:'1111', round:'1111', article:'1111' }, manager:{ group:'0110', user:'1111', player:'1111', game:'1111', round:'1111' }, trustee:{ group:'0100', user:'0110', player:'0110', game:'0110', round:'0110', }, ... other roles } def self.can(role) # called from User model - returns user roles cans = CRUD[role.downcase.to_sym] end ... some other utilities end The CRUD hash defines what Roles are used and what CRUD action each model implements. Each model defines a 4 character string of 0's or 1's - which equates to True or False. The manager role above: cannot Create or Destroy a Group, only Read or Update a group. Kind of simple . Every thing is controlled from the User model: class User (e) { e.strip.downcase } attribute :permits after_initialize :set_attributes def set_attributes self.permits = Can.can(self.role) unless self.role.blank? end def can?(action,model) return false if self.role.nil? || self.permits.nil? action = action.to_s.downcase model = model.to_s.downcase permit = permits[model.to_sym] return false if permit.nil? if ['create','new'].include?(action) return permit[0] == '1' elsif ['index','show','read'].include?(action) return permit[1] == '1' elsif ['edit','update'].include?(action) return permit[2] == '1' elsif ['delete','destroy'].include?(action) return permit[3] == '1' else return false end end end The User model has an attribute:permits defined and set after_initialize by calling set_attributes. set_attributes extracts the CRUD hash for the User's role and stuffs it into the permits attribute. It is there for the duration of the Session. Authorization takes place in the Model controller or a View. You add 9 lines to each Controller that is going to use Authorization: class UsersController

Mar 8, 2025 - 01:05
 0
Rails Basic Authorization (after Basic Authentication)

Hi! The hobby developer is back.

I've been doing RoR since version 0.9. I have a few semi-private apps deployed and try to keep up with versions.

When DHH released the Basic Authentication Scaffold I played with it and got it working. It did bother me that there is stuff in there that I have no idea on what it does. Like:

 def start_new_session_for(user)
  user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
  Current.session = session
    cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
  end
end

Have no idea what .tap does. Anyhow it works! Now on to a new challenge - Authorization.

I only have one app that has some form of role based Authorization. It's a golf group management app. It just kept track of groups, players, points, quotas etc. Kinda like a Handicap. I have several groups that use it. Each group has a user/manager that can do anything and a few that can do most things - form team - score teams etc. It turned out that they just share the User account and have two or three players that can do anything!

I think I tried Devise but didn't like the pain. Later tried CanCan and then CanCanCan. It was ok, but then I rolled my own. Didn't have much to do so I spent the last week rolling another version! I forgot to say that I just turned 80 and don't have much to do. Just though I'd share it.

Much like CanCanCan it has an Ability scheme that defines who can do what. In mine it's just a Constant wrapped in a class.

class Can
  CRUD = {
    super:{
      group:'1111',
      user:'1111',
      player:'1111',
      game:'1111',
      round:'1111',
      article:'1111'
    },
    manager:{
      group:'0110',
      user:'1111',
      player:'1111',
      game:'1111',
      round:'1111'
    },
    trustee:{
      group:'0100',
      user:'0110',
      player:'0110',
      game:'0110',
      round:'0110',
    },
    ... other roles
  }
  def self.can(role)
    # called from User model - returns user roles 
    cans = CRUD[role.downcase.to_sym]
  end
  ... some other utilities
end

The CRUD hash defines what Roles are used and what CRUD action each model implements. Each model defines a 4 character string of 0's or 1's - which equates to True or False. The manager role above: cannot Create or Destroy a Group, only Read or Update a group. Kind of simple .

Every thing is controlled from the User model:

 class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy
  normalizes :email_address, with: ->(e) { e.strip.downcase }

  attribute :permits
  after_initialize :set_attributes

  def set_attributes
    self.permits = Can.can(self.role) unless self.role.blank?
  end

  def can?(action,model)
    return false if self.role.nil? || self.permits.nil?
    action = action.to_s.downcase
    model = model.to_s.downcase
    permit = permits[model.to_sym]
    return false if permit.nil? 

    if ['create','new'].include?(action)
      return permit[0] == '1'
    elsif ['index','show','read'].include?(action)
      return permit[1] == '1'
    elsif ['edit','update'].include?(action)
      return permit[2] == '1'
    elsif ['delete','destroy'].include?(action)
      return permit[3] == '1'
    else
      return false
    end
  end

end

The User model has an attribute:permits defined and set after_initialize by calling set_attributes.

set_attributes extracts the CRUD hash for the User's role and stuffs it into the permits attribute. It is there for the duration of the Session.

Authorization takes place in the Model controller or a View.

You add 9 lines to each Controller that is going to use Authorization:

class UsersController < ApplicationController
  before_action :set_auth
  before_action :set_user, only: %i[ show edit update destroy ]

  # GET /users or /users.json
  def index
    @users = User.all
  end
  ...
    private
     def set_auth
      if ['index','show','new','edit','create','update','destroy'].include?(params["action"])
        permitted = Current.user.can?(params["action"],"user")
        unless permitted
          redirect_to dashboard_path, alert: "I'm sorry, but you can't do that!" 
        end
      end
    end
    ...

The before_action :set_auth calls the User.can?method with the params['action'] and the model name. Only the basic crud actions are filtered. Not sure if other action need to be included?? or needed?? Remember Simple!

On second though, you can wrap a controller action in a if/else statement or begin/rescue/end statement. Just Current.user.can?(:update,:model)

It's in the controller to prevent someone, who is not signed in, the typing in the browser /article/5/edit

On the View side you add code to links or buttons to hide them. You can add a helper to the application_controller and use it..

  def can?(action,model)
    Current.user && Current.user.can?(action,model)
  end
  helper_method :can?

Then add it to links in the view:

div
  = link_to icon('fas fa-eye',"Show User"), user,class:"btn-sm-green mr-4"
  = link_to icon('fas fa-edit',"Edit User"),  edit_user_path(user),class:"btn-sm-orange mr-4" if can?('edit',"user")

Going back to the User model, the can? method is called with an action and a model.
The action is more or less converted to CRUD. The show and index actions are just the R in CRUD or Read. That is done in the can? method.

Again I'm just a hobby developer. This was just a sharing effort. Maybe it will give someone some ideas.