0.0
No commit activity in last 3 years
No release in over 3 years
CrewdPolicies enables conventional Pundit (https://github.com/elabs/pundit) policies to be written using an opinionated pattern based on declarative Create, Read, Execute (optional), Write and Destroy (CREWD) permissions for each resource. Conventional pundit create?, show?, update? and destroy? permissions are automatically derived from these, as well as permitted_attributes/strong parameters.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.12
~> 10.0
~> 3.0

Runtime

>= 1.1.0, ~> 1.1
>= 0.1.4.0, ~> 0.1.4
 Project Readme

CrewdPolicies

CrewdPolicies enables conventional Pundit (https://github.com/elabs/pundit) policies to be written using an opinionated pattern based on declarative Create, Read, Execute (optional), Write and Destroy (CREWD) permissions for each resource. Conventional pundit create?, show?, update? and destroy? permissions are automatically derived from these, as well as permitted_attributes/strong parameters.

Installation

Add this line to your application's Gemfile:

gem 'crewd_policies'

And then execute:

$ bundle

Or install it yourself as:

$ gem install crewd_policies

Usage

The happy path that CREWD policies enables is as follows :

  1. include CrewdPolicies::Model into your models
class Person < ActiveRecord::Base
  include CrewdPolicies::Model
end
  1. declare constant arrays of field names, grouped to suit your application
USER_EDITABLE_FIELDS = [:name,:address]
ADMIN_FIELDS = [:roles]
ALL_FIELDS = ADMIN_FIELDS + USER_EDITABLE_FIELDS
  1. declare permissions using allow() and your constant arrays in your model
class Customer < ActiveRecord::Base
  include CrewdPolicies::Model
		
  PUBLIC_FIELDS = [:name]
  USER_EDITABLE_FIELDS = [:name,:address]
  ADMIN_FIELDS = [:roles]
  ALL_FIELDS = ADMIN_FIELDS + USER_EDITABLE_FIELDS
		 		
  allow :sales, :create => :this
  allow :sales, :read => ALL_FIELDS
  allow :sales, :write => USER_EDITABLE_FIELDS
		
  allow :admin, :write => ALL_FIELDS
  allow :admin, :destroy => :this 		
end
  1. include CrewdPolicies::Policy into your ApplicationPolicy or individual model policies. You will also need the Scope inner class defined on your application and/or individual model policies :
class ApplicationPolicy < Struct.new(:identity, :subject)
 include CrewdPolicies::Policy
   		
 class Scope < Struct.new(:identity, :scope)
   def resolve
     scope.where(...your criteria...)
   end
 end		
end 	
	 	
class CustomerPolicy < Struct.new(:identity, :subject)	
end
  1. your User or Identity model must have a has_role?(aRole) method

You now have a valid pundit policy that can be used like any other.

Parameters

aRole: a single string or symbol; or an array of strings and/or symbols aAbilities: a hash where -

  • keys are a single string or symbol; or an array of strings and/or symbols
  • values are true, or a single string or symbol; or an array of strings and/or symbols

Allow Syntax

The allow method is declared as :

def allow(aRole, aAbilities)
end

It is used on the model class as follows :

allow <role>, <abilities> => <fields>

Typical examples :

allow :sales, :index => true		# sales role can create any record in scope
allow [:finance, :marketing], [:create,:destroy,:index] => true
allow :sales, :read => :name
allow :sales, :read => [:address,:phone]
allow :reception, [:read,:write] => [:address,:phone]

Allow Conditions

An allow statement can be made conditional by adding an :if or :unless key. The value should be a symbol matching the name of a method with no parameters on the policy.

For example, we want users to be able to edit their own password :

on model :

allow :user, write: :password, if: :is_self?

on policy :

def is_self?
	record and !record.is_a?(Class) and record.id==identity.id
end

Note that without the if condition, any user would be able to write any user's password.

Required allow declarations for a full CRUD policy in Rails

In order to implement a policy that allows full CRUD on a resource in a Rails application, you will need to write allow declarations for each of the following abilities :

CREWD policy method Example Probably also needed
create? allow :user, create: true allow :user, :write => %w(name address)
read? allow :user, read: %w(name address)
write? allow :user, write: %w(name address password)
destroy? allow :user, destroy: true
index? allow :user, index: true allow :user, :read => %w(name address)

Note that for normal Rails CRUD requirements, fields are only declared for read and write, while true is only given for create, destroy and index. Your own abilities may be declared and queried with either true or an array of fields with no special requirements.

The above CREWD policy methods are then aliased to provide typical Rails policy methods as follows

Rails policy method CREWD policy method
create? create?
show? read?
update? write?
edit? write?
index? index?
delete? destroy?
destroy? destroy?

Other Pundit conventional Rails methods are also provided :

  • the following permitted attributes for "strong parameters" :
    • permitted_attributes (equivalent to permitted_attributes_for_write)
    • permitted_attributes_for_write
    • permitted_attributes_for_read
    • permitted_attributes_for_create
    • permitted_attributes_for_update
    • permitted_attributes_for_edit
    • permitted_attributes_for_show
    • permitted_attributes_for_index

This should meet the access control needs for the vast majority of Rails projects.

Controller Examples

def index	
  @posts = authorize policy_scope!(Post)
  # use per post @attributes = @post.attributes.slice permitted_attributes(@post)
end

def show	
  @post = authorize policy_scope!(Post).find(params[:id])	
  @attributes = @post.attributes.slice permitted_attributes(@post)
end

def create
  pars = params.require(:post).permit policy!(Post).permitted_attributes
  @post = authorize policy_scope!(Post).create!(pars)	
end

def update
  @post = authorize policy_scope!(Post).find(params[:id])
  pars = params.require(:post).permit policy!(Post).permitted_attributes
  @post.update_attributes(pars)
  @post.save!
end

def destroy
  @post = authorize policy_scope!(Post).find(params[:id])
  @post.destroy!
end

Core Assumptions

CREWD Policies builds policies based on the core assumption that by declaring the following permissions, a complete permissions system can be derived by code for 90+% of models down to the field level :

- scope for the resource 
- create for the resource 
- readable fields
- writeable fields  
- delete for a record
- normal Rails model validations for validating field values

Expanding on the above :

  1. The relevant policy scope should be used as a normal practice for all operations, unless there is a good reason. Rails scopes limit access for select, update and delete queries; and set default values for insert queries. The other permissions assume the proper scope has been applied.

  2. create? permission requires :

    1. a resource level permission (ie. "Can this role create customers at all?")
    2. field level write permissions (ie. "When creating customers, what fields can be provided by this role?"
    3. field values that pass the normal Rails model validations - this is left to the user and out of the scope of this gem.
  3. read? permission requires :

    1. at least 1 readable field
  4. write? permission requires :

    1. at least 1 writeable field
    2. field values that pass the normal Rails model validations - this is left to the user and out of the scope of this gem.
  5. destroy? permission requires :

    1. a record level permission (ie. "Can this role destroy this customer?")

User/Identity Model Assumptions

  1. User or Identity Model : Traditional Rails applications have a User model which maps to a database table of users. An emerging architecture pattern uses JSON Web Tokens (http://jwt.io) to represent an identity managed by an external provider. Applications then will typically need an additional model eg. Person for attaching persisted data to that provided by the identity token. I have had success creating an Identity model; not backed by the database but created in memory by decoding the JWT. It then has methods for loading a Person model if required. This is how we intend to do things in future, and so the property name I am using here is identity, but I also use an alias of user pointing referring to it.

  2. identity.has_role?(aRole) : In order to interrogate the roles assigned the identity has, the method has_role?(aRole) must be implemented to receive a role string or symbol, and return true or false.

Why Pundit::NotAuthorizedError is misleading

Pundit defines this error, and raises it when the authorize method rejects a query. Unfortunately, in this case, Pundit users could easily assume they should return the HTTP status 401 Unauthorized, but this would be against the definition for this status code.

"The request has not been applied because it lacks valid authentication credentials for the target resource" - https://httpstatuses.com/401

Failing pundit checks rarely has anything to do with a lack of credentials, the failure is more likely a case of

"The server understood the request but refuses to authorize it." - https://httpstatuses.com/403

It gets even worse if you follow a pattern in your client of forcing a logout of the user when they receive a 401, which makes sense when 401 is used correctly. The result is that attempting anything not allowed by a policy causes the user to be logged out, when they should simply be shown an alert and given the opportunity to correct the error or do something else while maintaining their session.

Pundit does have this in the README https://github.com/elabs/pundit#rescuing-a-denied-authorization-in-rails, but it is easily missed and naming mismatch is still likely to trip up new users.

varvet/pundit#412

As an initial mitigation, crewd-policies provides the CrewdPolicies::ForbiddenError exception and the forbidden! method.

Development

After checking out the repo, to install dependencies run:

bin/setup

Then, run tests with:

rake spec

For an interactive prompt that will allow you to experiment, you can also run

bin/console

To install this gem onto your local machine, run

bundle exec rake install

To release a new version, update the version number in version.rb, and then run

bundle exec rake release

rake release will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

To experiment with this gem with interactive prompt, run

 bin/console

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/crewd_policies. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.