Simple functional authorization library and role managment for ruby. Inspired by transproc and dry project
Kan provide simple role system. It will detect all abilities object where role is true. For this you need to define role block in each abilities classes:
module Post
  class AnonymousAbilities
    include Kan::Abilities
    role(:anonymous) do |user, _|
      user.id.nil?
    end
    register(:read, :edit, :delete) { false }
  end
  class BaseAbilities
    include Kan::Abilities
    role(:all) do |_, _|
      true
    end
    register(:read) { |_, _| true }
    register(:edit, :delete) { |user, post| false }
  end
  class AuthorAbilities
    include Kan::Abilities
    role(:author) do |user, post|
      user.id == post.author_id
    end
    register(:read, :edit) { |_, _| true }
    register(:delete) { |_, _| false }
  end
  class AdminAbilities
    include Kan::Abilities
    role(:admin) do |user, _|
      user.admin?
    end
    register(:read, :edit, :delete) { |_, _| true }
  end
end
After that initialize Kan application object and call it with payload:
abilities = Kan::Application.new(
  post: [Post::AnonymousAbilities.new, Post::BaseAbilities.new, Post::AuthorAbilities.new, Post::AdminAbilities.new],
  comment: Comments::Abilities.new
)
abilities['post.read'].call(anonymous, post) # => role: true (anonymous), result: false
abilities['post.read'].call(regular, post)   # => role: true (base), result: true
abilities['post.read'].call(author, post)    # => role: true (author), result: true
abilities['post.read'].call(admin, post)     # => role: true (admin), result: true
abilities['post.edit'].call(anonymous, post) # => false
abilities['post.edit'].call(regular, post)   # => false
abilities['post.edit'].call(author, post)    # => true
abilities['post.edit'].call(admin, post)     # => true
abilities['post.delete'].call(anonymous, post) # => false
abilities['post.delete'].call(regular, post)   # => false
abilities['post.delete'].call(author, post)    # => false
abilities['post.delete'].call(admin, post)     # => true
Kan allow to use classes as roles for incapulate and easily testing your roles.
module Post
  module Roles
    class Admin
      def call(user, _)
        user.admin?
      end
    end
    class Anonymous
      def call(user, _)
        user.id.nil?
      end
    end
  end
  class AnonymousAbilities
    include Kan::Abilities
    role :anonymous, Anonymous
    register(:read, :edit, :delete) { false }
  end
  class AdminAbilities
    include Kan::Abilities
    role :admin, Roles::Admin
    register(:read, :edit, :delete) { |_, _| true }
  end
end
Kan allow to use “callable” (objects with #call method) as a role object. For this just put it into ability class:
module Post
  module Roles
    class Admin
      def call(user, _)
        user.admin?
      end
    end
    class Anonymous
      def call(user, _)
        user.id.nil?
      end
    end
  end
  class AnonymousAbilities
    include Kan::Abilities
    role :anonymous, Roles::Anonymous.new
    register(:read, :edit, :delete) { false }
  end
  class AdminAbilities
    include Kan::Abilities
    role :admin, Container['post.roles.admin'] # or use dry-container
    register(:read, :edit, :delete) { |_, _| true }
  end
end
Kan allow to detect all roles for specific payload. For this use roles calls in scope:
module Post
  class AnonymousAbilities
    include Kan::Abilities
    role :anonymous, Roles::Anonymous.new
    register(:read, :edit, :delete) { false }
  end
  class AdminAbilities
    include Kan::Abilities
    role :admin, Roles::Admin.new
    register(:read, :edit, :delete) { |_, _| true }
  end
end
abilities = Kan::Application.new(
  post: [AnonymousAbilities.new, AdminAbilities.new]
)
abilities['post.roles'].call(anonymous_user, payload) # => [:anonymous]
abilities['post.roles'].call(admin_user, payload)     # => [:anonymous, :admin]