Protector is a Ruby ORM extension for managing security restrictions on a field level. The gem favors white-listing over black-listing (everything is disallowed by default), convention over configuration and is duck-type compatible with most of existing code.
Currently Protector supports the following ORM adapters:
We are working hard to extend the list with:
Protector is an extension and therefore hides deeply inside your ORM library making itself compatible to the most gems you use. Sometimes however, you might need additional integration to take the best from it:
DSL of Protector is a Ruby block (or several) describing ACL separated into contexts (authorized user is a very typical example of a context). Each time the context of model changes, DSL blocks reevaluate internally to get an actual ACL that is then utilized internally to cut restricted actions.
Protector follows nondestructive blocking strategy. It returns nil
when the forbidden field is requested and only checks creation (modification) capability during persisting. Even more: the latter is implemented as a model validation so it will seamlessly integrate into your typical workflow.
This example is based on ActiveRecord but the code is mostly identical for any supported adapter.
class Article < ActiveRecord::Base # Fields: title, text, user_id, hidden
protect do |user| # `user` is a context of security
if user.admin?
scope { all } # Admins can retrieve anything
can :read # ... and view anything
can :create # ... and create anything
can :update # ... and update anything
can :destroy # ... and they can delete
else
scope { where(hidden: false) } # Non-admins can only read insecure data
can :read # Allow to read any field
if user.nil? # User is unknown and therefore not authenticated
cannot :read, :text # Guests can't read the text
end
can :create, %w(title text) # Non-admins can't set `hidden` flag
can :create, user_id: labmda{|x| # ... and should correctly fill
x == user.id # ... the `user_id` association
}
# In this setup non-admins can not destroy or update existing records.
end
end
end
Inside your model, you can have several protect
calls that will get merged. Using this you can move basic rules to a separate module to keep code DRY.
Now that we have ACL described we can enable it as easy as:
article.restrict!(current_user) # Assuming article is an instance of Article
If current_user
is a guest we will get nil
from article.text
. At the same time we will get validation error if we pass any fields but title, text and user_id (equal to our own id) on creation.
To make model unsafe again call:
article.unrestrict!
Both methods are chainable!
Besides the can
and cannot
directives Protector also handles relations visibility. In the previous sample the following block is responsible to make hidden articles actually hide:
scope { where(hidden: false) } # Non-admins can only read unsecure data
Make sure to write the block content of the scope
directive in the notation of your ORM library.
To finally utilize this function use the same restrict!
method on a level of Class or Relation. Like this:
Article.restrict!(current_user).where(...)
# OR
Article.where(...).restrict!(current_user)
Be aware that if you already made the database query the scope has no effect on the already fatched data. This is because Protector is working on two levels: first during retrieval (scops are applied here) and after that on the level of fields. So for example find
and restrict!
calls are not commutative:
# Should be used if you are using scops for visibility restriction
Article.restrict!(current_user).find(3)
# not equal!
# Will select the record with id: 3 regardless of any scops and only restrict on the field level
Article.find(3).restrict!(current_user)
Note also that you don't need to explicitly restrict models you get from a restricted scope – they born restricted.
Important: unlike fields, scopes follow black-list approach by default. It means that you will NOT restrict selection in any way if no scope was set within protection block! This arguably is the best default strategy. But it's not the only one – see paranoid
at the list of available options for details.
Sometimes an access decision depends on the object we restrict. protect
block accepts second argument to fulfill these cases. Keep in mind however that it's not always accessible: we don't have any instance for the restriction of relation and therefore nil
is passed.
The following example extends Article to allow users edit their own posts:
class Article < ActiveRecord::Base # Fields: title, text, user_id, hidden
protect do |user, article|
if user
if article.try(:user_id) == user.id # Checks belonging keeping possible nil in mind
can :update, %w(title text) # Allow authors to modify posts
end
end
end
end
Protector is aware of associations. All the associations retrieved from restricted instance will automatically be restricted to the same context. Therefore you don't have to do anything special – it will respect proper scopes out of the box:
foo.restrict!(current_user).bar # bar is automatically restricted by `current_user`
Remember however that auto-restriction is only enabled for reading. Passing a model (or an array of those) to an association will not auto-restrict it. You should handle it manually.
The access to belongs_to
kind of association depends on corresponding foreign key readability.
Both of eager loading strategies (separate query and JOIN) are fully supported.
Each restricted model responds to the following methods:
visible?
– determines if the model is visible through restriction scopecreatable?
– determines if you pass validation on creation with the fields you setupdatable?
– determines if you pass validation on update with the fields you changeddestroyable?
– determines if you can destroy the modelIn fact Protector does not limit you to :read
, :update
and :create
actions. They are just used internally. You however can define any other to make custom roles and restrictions. All of them are able to work on a field level.
protect do
can :drink, :field1 # Allows `drink` action with field1
can :eat # Allows `eat` action with any field
end
To check against custom actions use can?
method:
model.can?(:drink, :field2) # Checks if model can drink field2
model.can?(:drink) # Checks if model can drink any field
As you can see you don't have to use fields. You can use can :foo
and can? :foo
. While they will bound to fields internally it will work like you expect for empty sets.
Sometimes for different reasons (like debug or whatever) you might want to run piece of code having Protector totally disabled. There is a way to do that:
Protector.insecurely do
# anything here
end
No matter what happens inside, all your entities will act unprotected. So use with EXTREME caution.
Please note also that we are talking about "unprotected" and "disabled". It does not make can?
to always return true
. Instead can?
would thrown an exception just like it does for any unprotected model. Any other approach makes logic incostitent, unpredictable and just dangerous. There are different possible strategies to isolate business logic from security domain in tests like direct can?
mocking or forcing admin role to a test user. Use them whenever you want to abstract from security in a whole and insecurely
when you want to mock a model to the basic security state.
Protector is a successor to Heimdallr. The latter being a proof-of-concept appeared to be way too paranoid and incompatible with the rest of the world. Protector re-implements same idea keeping the Ruby way:
The last thing is really important to understand. No matter if you can read a field or not, methods like .pluck
are still capable of reading any of your fields and if you tell your model to skip validation it will also skip an ACL check.
Add this line to your application's Gemfile:
gem 'protector'
And then execute:
$ bundle
Or install it yourself as:
$ gem install protector
As long as you load Protector after an ORM library it is supposed to activate itself automatically. Otherwise you can enable required adapter manually:
Protector::Adapters::ActiveRecord.activate!
Where "ActiveRecord" is the adapter you are about to use. It can be "Sequel", "DataMapper", "Mongoid".
Use Protector.config.option = value
to assign an option. Available options are:
true
will force Protector to return empty scope when no scope was given within a protection block.false
to disable built-in Strong Parameters integration.Protector features basic Rails integration so you can assign options using config.protector.option = value
at your config/*.rb
.
protector
.It is free software, and may be redistributed under the terms of MIT license.