0.0
A long-lived project that still receives updates
Bouncer brings simple authorization to Sinatra.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 2.2
 Project Readme

Sinatra-Bouncer

Simple permissions extension for Sinatra. Require the gem, then declare which routes are permitted based on your own logic.

Big Picture

Bouncer's syntax looks like:

# You can define roles to collect permissions together
bouncer.role :admins do
   current_user&.admin?
end

bouncer.rules do
   # Routes match based on one or more strings. 
   anyone.can get:  '/',
              post: ['/user/sign-in',
                     '/user/sign-out']

   # Wildcards match anything directly under that path 
   anyone.can get: '/lib/js/*'

   admins.can get:  '/admin/*',
              post: '/admin/actions/*'

   # ... etc ...
end

Features

Here's what this Gem provides:

  • Block-by-default
    • Any route must be explicitly allowed
  • Role-Oriented
    • The DSL is constructed to be easily readable
  • Declarative Syntax
    • Straightforward syntax reduces complexity of layered permissions
    • Keeps permissions together for clarity
  • Conditional Logic Via Blocks
    • Often additional checks must be performed to know if a route is allowed.
  • Grouping
    • Routes can be matched by wildcard
  • Forced Boolean Affirmation
    • Condition blocks must explicitly return true, avoiding accidental truthy values

Anti-Features

Bouncer intentionally does not support some concepts.

  • No User Identification (aka Authentication)
    • Bouncer is not a user identification gem. It assumes you already have an existing solution like Warden. Knowing who someone is is already a complex enough problem and should be separate from what they may do.
  • No Rails Integration
    • Bouncer is not intended to work with Rails. Use one of the Rails-based permissions gems for these situations.
  • No Negation
    • There is intentionally no negation (eg. cannot) to preserve the default-deny frame of mind

Installation

Add this to your gemfile:

gem 'sinatra-bouncer'

Then run:

bundle install

Sinatra Modular Style

require 'sinatra/base'
require 'sinatra/bouncer'

class MyApp < Sinatra::Base
   register Sinatra::Bouncer

   bouncer.rules do
      # ... can statements ...
   end

   # ... routes and other config
end

Sinatra Classic Style

require 'sinatra'
require 'sinatra/bouncer'

bouncer.rules do
   # ... can statements ...
end

# ... routes and other config

Usage

Define roles using the role method and provide each one with a condition block. Then call rules with a block that uses your defined roles with the #can or #can_sometimes DSL methods to declare which paths are allowed.

The rules block is run once as part of the configuration phase but the condition blocks are evaluated in the context of the request handler. This means they have access to Sinatra helpers, the request object, and params.

require 'sinatra'
require 'sinatra/bouncer'

bouncer.role :members do
   !current_user.nil?
end

bouncer.role :bots do
   !request.get_header('X-CUSTOM-BOT').nil?
end

bouncer.rules do
   # example: always allow GET requests to '/' and '/about'; and POST requests to '/sign-in'
   anyone.can get:  ['/', '/about'],
              post: '/sign-in'

   # example: logged in users can view (GET) member restricted paths and edit their account (POST)
   members.can get: '/members/*'
   members.can_sometimes post: '/members/edit-account' do
      current_user && current_user.id == params[:id]
   end

   # example: require presence of arbitrary request header in the role condition
   bots.can get: '/bots/*'
end

# ... Sinatra route declarations as normal ... 

Role Declarations

Roles are declared using the #role method in your Sinatra config. Each one must be provided a condition block that must return exactly true when the role applies.

# let's pretend that current_user is a helper that returns the user from Warden
bouncer.role :admins do
   current_user&.admin?
end

Note: There is a default role called anyone that is always declared for you.

HTTP Method and Route Matching

Both #can and #can_sometimes accept symbol HTTP methods as keys and each key is paired with one or more path strings.

# example: single method, single route 
anyone.can get: '/'

# example: multiple methods, single route each 
anyone.can get:  '/',
           post: '/blog/action/save'

# example: multiple methods, multiple routes (using string array syntax)
anyone.can get:  %w[/
                    /sign-in
                    /blog/editor],
           post: %w[/blog/action/save 
                    /blog/action/delete]

Note Allowing GET implies allowing HEAD, since HEAD is by spec a GET without a response body. The reverse is not true, however; allowing HEAD will not also allow GET.

Wildcards and Special Symbols

Provide a wildcard * to match any string excluding slash. There is intentionally no syntax for matching wildcards recursively, so nested paths will also need to be declared.

# example: match anything directly under the /members/ path
members.can get: '/members/*'

There is also a special symbol, :all that matches all paths. It is intended for rare use with superadmin-type accounts.

# this allows GET on all paths to those in the admin group
admins.can get: :all

Warning Always be cautious when using wildcards and special symbols to avoid accidentally opening up pathways that should remain private.

Always Allow: can

Any route declared with #can will be accepted without further challenge.

bouncer.rules do
   # Anyone can access this path over GET
   anyone.can get: '/login'
end

Conditionally Allow: can_sometimes

can_sometimes takes a condition block that will be run once the path is attempted. This block must return an explicit boolean (ie. true or false) to avoid any accidental truthy values creating unwanted access.

bouncer.role :users do
   !current_user.nil?
end

bouncer.rules do
   users.can_sometimes post: '/user/save' do
      current_user.id == params[:id]
   end
end

Custom Bounce Behaviour

The default bounce action is to halt 403. Call bounce_with with a block to specify your own behaviour. The block is also run in a Sinatra request handler context, so you can use helpers here as well.

require 'sinatra'
require 'sinatra/bouncer'

bounce_with do
   redirect '/login'
end

# bouncer rules, routes, etc...

Alternatives

The syntax for Bouncer is largely influenced by the now-deprecated CanCan gem.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/TenjinInc/Sinatra-Bouncer.

Valued topics:

  • Error messages (clarity, hinting)
  • Documentation
  • API
  • Security correctness

This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct. Play nice.

Core Developers

After checking out the repo, run bundle install to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

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, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Documentation is produced by Yard. Run bundle exec rake yard. The goal is to have 100% documentation coverage and 100% test coverage.

Release notes are provided in RELEASE_NOTES.md, and should vaguely follow Keep A Changelog recommendations.

License

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