Kantox::Roles

Kantox Roles is the library to transparently handle an authorization. It is fully backend-agnostic. Current implementation contains a working example of pundit wrapping.

The main goal is to separate the wheat from the chaff and not to pollute model/controller classes with authorization stuff.

With Kantox Roles one is to define the rules in one or more yaml files:

'Kantox::Managed':
  :yo : 'aspect'
  :yo2 : :aspect
  :yo3 :
    :runner: 'Kantox::Strategies::Wrapper#wrap'
  :yo5 :
    :lambda: '->(context, im) { 42 }'
  :yo6 :
    :kantox_managedhandler:
      :params: ['param1', 'param2']
'Kantox::WildManaged':
  'y*' : :aspect

There are four different kinds of handlers available:

  • simple — like aspect above. There should be Kantox::Strategies#aspect module function available. It will be called with parameters context and im supplying the context instance (usually an instance of guarded controller class) and the name of the guarded method in Module::Class#method notation. The aforementioned function should raise an exception of type Kantox::Strategies::StrategyError whether the authority check is not passed.
  • runner — the most powerful yet complicated guard. Should have the executable module function as parameter in Module::Class#method notation. This function will be called, yielding context, im and the guarded method, converted to proc as parameters. The typical usage:
def my_runner context, params = nil
  fail Kantox::Strategies::MyRunnerError unless check_passed
  params[:user] = :demo if demo_mode
  params[:credit_card].gsub /\d/, 'X'
  yield *params if block_given?
end

As seen above, the full control over context is provided by this guard.

  • lambda — the simple lambda, getting context and im. Is actually a syntax sugar for the simple guard
  • object — used when there is a need to pass complicated parameters and/or some other stuff to the guard. The class Kantox::Managedhandler must have a constructor, accepting one argument (the hash of parameters) and to_proc method, accepting two arguments (context, im). The class will be instantiated with a parameters list and to_proc would be called on context.

Methods to guard

Wildcard notation is allowed. That said, 'y*' will guard all the methods starting with y, and '*' will guard everything on the respective class.

Deny ⇒ Allow

As soon as a method guard is specified in yaml file, the method is considered as guarded. If there was an error finding the guard (class not exists, method can not be instantiated etc,) the authorization request will be rejected.

Rails integration

# controllers/admin/admin_controller.rb

require 'kantox/roles'
# Specify the top-parent class to guard
Kantox::Roles.init Admin::AdminController
# load strategies
Dir['strategies/**/*.yml'].each do |f|
  Kantox::Roles.configure f
end
# load stopwords for logger
Kantox::Helpers.logger_stopwords File.join 'strategies', 'stopwords.txt'
Kantox::Helpers.info "Strategies were read: #{Kantox::Roles.options}"
# config/application.rb

# Policies are to be loaded on init explicitly
Dir[File.expand_path('../../app/policies/**/*', __FILE__)].each do |f|
  require f[/(.*?)\.rb$/, 1] unless File.directory? f
end
# strategiest/roles.yml

'Admin::TodosController' :
  '*' : :pundit
...

Pundit plug-in

This section shows how to integrate pundit to act as backend guard. Everything one needs is to implement policies.

Everything in TodosController is to be handled by pundit. So, it’s time to implement our pundit guard. Besides some syntax sugar stuff it is (complete implementation for kantox-flow may be seen here:

# app/policies/pundit.rb

def pundit context, im
  begin # Whoever does not reply on classify would be punished :)
    model = context.instance_eval 'controller_path.classify'
    policy = PolicyFactory.lookup model.split('::').last
    unless policy.new(context.current_user, model).send("#{im.split('#').last}?")
      fail PunditError.new(context, im, policy)
    end
  rescue NameError => e
    Kantox::Helpers.err "Error punditing «#{context}». Will reject request.\nOriginal error: #{e}"
    throw PunditError.new(context, im)
  end
end
module_function :pundit

Needless to say, the above handler is to be written once per backend. Pundit one is shipped with Kantox::Roles. The last but not least is to specify a strategy:

# app/policies/todo_policy.rb

module Kantox
  module Policies
      def historic?
        @user.admin? and [true, false].sample
      end
      alias_method :index?, :historic?
    end
  end
end

The above will randomly accept admin’s requests to a controller. I am pretty sure one would do more sophisticated check here.

That’s it.

Policies Generator

Kantox::Roles has it’s generator for creating policies.

Generator invocation

$ bundle exec rails generate kantox:pundit_policy Todo --users Administrator SalesPerson

The above will generate (assuming that Todo is a proper model name and TodosController exists:

  • Policy itself, in app/policies,
  • Spec for a policy in spec/policies,
  • Default strategy in strategies.
  • [optional] if this is a first run, the pundit:install generator will be invoked to generate default pundit ApplicationPolicy.

By default all the public controller methods will be guarded with method? pundit guards, returning true if the current user was listed during generation process.

Generate Policy

Generated specs

The generator above would generate smart specs, more or less ready to use. For an example above, it would generate specs, checking whether all the specified users are allowed to access the controller, and all others are restricted.

Generated Specs

Vandal-proof

The generator will gracefully reject a request to damage anything as well as to generate policies for inexisting controller.

Vandal-proof

Installation

gem 'kantox-roles'

TODOs

  • generate policies and rspecs by yaml
  • ~~pointcuts syntax — DSL vs YAML/JSON~~
  • automatic tests for pointcuts — by adding them to descriptions
  • allow⇒deny vs deny⇒allow
  • ~~supersupervisor to change rights of supervisors~~
  • ~~groups with roles, or smth morre sophisticated? What?~~
  • ~~where to store rights? 3rd party?~~
  • edit rights from the interface.

Usage

Add a line to configuration file (or explicitly by calling Kantox::Roles.configure with either hash, or passing a block.)

Optionally, one may specify a custom handler as denoted by :runner key in config.

Development

After checking out the repo, run bin/setup to install dependencies. Then, 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 to create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/kantox-roles/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request