If you like the straight forward and effective nature of Strong Parameters and suspect that cancan might be overkill for your project then you'll love Petergate's easy to use and read action and content based authorizations.
-- 1 Peter 3:41
Rails 7.1 through 8.1 on Ruby 3.2 through 3.4 are covered by CI. Older Rails versions are permitted by the gemspec but are not verified.
Add this line to your application's Gemfile:
gem 'petergate'
And then execute:
bundle
Or install it yourself as:
gem install petergate
The generator writes into app/models/user.rb, so a model named User is the
supported setup.
If you're using devise you're in luck, otherwise you'll have to add the following methods to your project:
current_user
after_sign_in_path_for(current_user)
authenticate_user!
You also need a root route: a refused visitor who isn't signed in is sent
there.
rails g petergate:install
rake db:migrate
This will add a migration and insert petergate into your User model.
Configure available roles by modifying this block at the top of your user.rb.
############################################################################################
## PeterGate Roles ##
## The :user role is added by default and shouldn't be included in this list. ##
## The :root_admin can access any page regardless of access settings. Use with caution! ##
## The multiple option can be set to true if you need users to have multiple roles. ##
petergate(roles: [:admin, :editor], multiple: false) ##
############################################################################################With multiple: false the role is stored in the column as a plain string. With
multiple: true the roles are stored as a YAML array, so query them through the
generated scopes below rather than by matching the column directly.
user.role => :editor
user.roles => [:editor, :user]
user.roles=(v) #sets roles
user.available_roles => [:admin, :editor, :user]
user.has_roles?(:admin, :editor) # true if the user has any of the roles passed in
user.has_role?(:admin) # alias of has_roles?A scope is defined for each configured role, named role_ plus the pluralized
role name:
User.role_admins # => users holding :admin
User.role_editors # => users holding :editorSo petergate(roles: [:admin, :teacher]) gives you User.role_admins and
User.role_teachers.
Setup permissions in your controllers the same as you would for a before filter like so:
access all: [:show, :index], user: {except: [:destroy]}, company_admin: :all
# one other option that might seem a bit weird is to put a group of roles in an array:
access [:all, :user] => [:show, :index]The key is a role, or an array of roles. all covers everyone, including
visitors who aren't signed in. The value is one of:
| Value | Meaning |
|---|---|
[:show, :index] |
just those actions |
:all |
every action on the controller |
{except: [:destroy]} |
every action except those |
:root_admin is not a rule you write -- a user holding it bypasses the rules
entirely.
Rules declared on a parent controller are inherited by its subclasses, so a
single access line on ApplicationController can cover a whole app.
access works the same way in an ActionController::API controller. There a
refused request answers with a bare 403, and an unauthenticated one with
401, instead of redirecting.
Inside your views you can use logged_in?(:admin, :customer, :etc) to show or hide content.
<%= link_to "destroy", destroy_listing_path(listing) if logged_in?(:admin, :customer, :etc) %>logged_in? tests roles. To ask only whether anyone is signed in, without
caring which role they hold, use user_logged_in?.
If you need to access available roles within your project you can by calling:
User::ROLES # => [:admin, :editor, :user]
User.first.available_roles # the same list, from an instanceROLES is a constant on the model, so it is also reachable from your own
instance methods. A subclass shares its parent's roles.
Two helpers are available in controllers and in views:
forbidden! # refuse someone who is signed in
unauthorized! # send a visitor to authentication, via authenticate_user!forbidden! is the one you want in your own filters:
before_action :check_active_user
def check_active_user
forbidden! unless current_user.active
endBoth answer a js, json or xml request with a bare 403 or 401 rather
than a redirect, and do the same in an ActionController::API controller,
which has no format negotiation to offer.
forbidden! takes one for a single call, and access sets a default for the
whole controller:
forbidden! "Your account is suspended"
access user: [:show, :index], message: "You shall not pass"The message is resolved in this order, first match winning:
| Source | |
|---|---|
| 1 | the argument passed to forbidden! |
| 2 | an msg request header |
| 3 | the message: option on access |
| 4 | "Permission Denied" |
Note the second entry: the msg header is read off the request, so a caller
can replace a message you set with message:. It is undocumented legacy
behaviour rather than something to rely on.
= form_with model: @user do |f|
- if @user.errors.any?
#error_explanation
h2 = "#{pluralize(@user.errors.count, "error")} prohibited this user from being saved:"
ul
- @user.errors.full_messages.each do |message|
li = message
.field
= f.label :email
= f.text_field :email
- if @user.new_record? || params[:passwd]
.field
= f.label :password
= f.password_field :password
.field
= f.label :password_confirmation
= f.password_field :password_confirmation
.field
= f.label :roles
= f.select :roles, @user.available_roles, {}, {multiple: true}
.actions = f.submit = form_with model: @user do |f|
- if @user.errors.any?
#error_explanation
h2 = "#{pluralize(@user.errors.count, "error")} prohibited this user from being saved:"
ul
- @user.errors.full_messages.each do |message|
li = message
.field
= f.label :email
= f.text_field :email
- if @user.new_record? || params[:passwd]
.field
= f.label :password
= f.password_field :password
.field
= f.label :password_confirmation
= f.password_field :password_confirmation
.field
= f.label :role
= f.select :role, @user.available_roles
.actions = f.submit bundle install
bundle exec rake test
The suite boots a small Rails application in memory rather than carrying a dummy app. To run it against a specific Rails version:
BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
PeterGate is written and maintained by Isaac Sloan and friends.
- Fork it ( https://github.com/elorest/petergate/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
