0.01
Low commit activity in last 3 years
A long-lived project that still receives updates
Solution that eliminates the tight coupling between user identity/authentication and application functional authorization
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

~> 3.0
> 4.0
 Project Readme

Gem Version Build Status

Overview

The purpose of Invitational is to eliminate the tight coupling between user identity/authentication and application functional authorization. It is a common pattern in multi-user systems that in order to grant access to someone else, an existing administrator must create a user account, providing a username and password, and then grant permissions to that account. The administrator then needs to communicate the username and password to the individual, often via email. The complexity of this process is compounded in multi-account based systems where a single user might wind up with mutiple user accounts with various usernames and passwords.

Inspired by 37Signals' single sign-on process for Basecamp, Invitational provides an intermediate layer between an identity model (i.e. User) and either the system as a whole or some specific entity to which authorization is given. This intermediate layer, an Invitation, represents a granted role for the sytem or a given entity. These roles can then be leveraged by the application's functional authorization system.

Invitational supplies a custom DSL on top of the CanCan gem to provide an easy implementation of role-based functional authorization. This DSL supports the hierarchical model common in many systems. Permissions can be esablished for a child based upon an invitation to its parent (or grandparent, etc).

An invitation is initially created in an un-claimed state. The invitation is associated with an email address, but can be claimed by any user who has the unique claim hash. The Invitational library allows for this delegation of authority, though it is fully possible for a host application to implement a requirement that the user claiming an invitation must match the email for which the invitation was created. Once claimed, an invitation may not be claimed again by any other user.

Getting Started

Invitational works with Rails 4.0 and up. You can add it to your Gemfile with:

gem 'invitational'

Run the bundle command to install it.

After you install the gem, you need to run the generator:

rails generate invitational:install MODEL

Replace MODEL with the class name of your identity class. Since this is very frequently User, the generator defaults to that class name, thus you can omit it if that is how your application is built:

rails generate invitational:install

The generator will add a database migration you will need to run:

rake db:migrate

Types of Invitations

Invitational has three types of invitations:

Entity

An Entity invitation, as the name imples, is for a specific entity within the system. For example, in a contract management system, a user might be invited to a contract in the sytem with the role of 'Recipient' . They might then be able to read and to mark that specific contract as signed, but not access any other contracts in the system.

System

A System invitation is not related to a specific entity, but to the system overall. For example, in the contract management system mentioned above, another user might be invited to the sytem with the role of 'contract_manager'. They might then be able to manage all contracts within the system, but not have authority to invite other users.

UberAdmin

An UberAdmin invitation is also, like a System invitation, not related to a specific entity but to the system overall. Unlike a System invitation, an UberAdmin invitation effectively grants the associated user access to all parts of the system, as every inquiry for the existance of an invitation (either System or Entity) will indicate true.

Implementation

invited_to

The generator will setup your identity model (User) to include the Invitational::InvitedTo module. As part of the Invitational functionality it provides, the invited_to method is added to your user class along with the foundational has_many relationship to Invitation. This method accepts a list of the entity classes (as symbols) to which a user can be invited:

invited_to :customer, :vendor, :supplier

This will setup has_many :through relationships for each entity:

user.companies
user.vendors
user.suppliers

accepts_invitation_for

To configure an entity as able to accept invitations, use the make_invitable generator:

rails generate invitational:make_invitable MODEL, ROLE1, ROLE2...

Here, replace MODEL with the name of the entity class you are making invitable. Replace, ROLE1, ROLE2 with the list of roles which are valid to this model, for example User, Admin. The generator will include the Invitational::AcceptsInvitationAs module, and will pre-populate the call to the accepts_invitation_as method with the list of roles supplied:

accepts_invitation_as :user, :admin

As with your identity class, a foundational has_many relationship is established with Invitation. The accepts_invitation_as method also sets up has_many :through relationships to user for each role identified:

entity.users
entity.admins

You can then add this entity to the list of invitable classes on the invited_to call in your identity class.

accepts_system_roles_as

System roles are defined in the Invitation class. Simply add the list of system roles to the class method that has been defined for you by the generator:

accepts_system_roles_as :contract_manager, :bookkeeper

The accepts_system_roles_as method also sets up scopes on Invitation for each identified role:

License.contract_managers
License.bookkeepers

Similarly, the identity model is given scopes for each role:

User.uberadmins # returns users who are uberadmins
User.contract_managers # returns users with the contract_manager system role
User.bookkeepers # returns users with the bookkeepers system role

Usage

Creating Invitations

To create an entity invitation to a given model:

entity = Entity.find(1)

entity.invite "foo@bar.com", :admin

To create an invitation to a system role:

Invitation.invite_system_user "foo@bar.com", :contract_manager

The method will return the Invitation. In the event that the email has already been invited to that entity or to the system role, an Invitational::AlreadyInvitedError will be raised. If the passed role is not valid for the given entity (based on its accepts_invitation_as call) or not a valid system role, an Invitational::InvalidRoleError will be raised.

Immediately Claimed Invitations

In some situations it is preferable to have an invitation created that is immediately claimed by an existing user. For example, if the current user is creating an invitable entity, they would likely want to have immediate administrative authority to that entity. In such situations, you can pass a user object (an instance of your identity class) to the invite method instead of an email. The invitation that is created will be immedately claimed by that user:

entity = Entity.create(...)

entity.invite current_user, :admin

and

Invitation.invite_system_user current_user, :contract_manager

Claiming Invitations

Invitations can be claimed by passing their hash and the claiming user to the claim class method on Invitation:

Invitation.claim claim_hash, current_user

The method will return the claimed Invitation. In the event that the hash does match an existing invitation, an Invitational::InvitationNotFoundError will be raised. If the hash is found, but the invitation has already been claimed, an Invitational::AlreadyClaimedError will be raised.

Checking for Invitations

The invited_to? instance method that Invitational adds to your identity class provides an easy interface to check if a user has an accepted invitation to a specific entity. Your query can be general (invited in any role) or specifically for a supplied role:

current_user.invited_to? entity

Will return true if the current user has accepted an invitation in any role to the entity.

current_user.invited_to? entity, :admin

Will only return true if the current user has accepted an invitation as an Admin to the entity.

For system roles, the invited_to_system? instance method on your identity class can be used:

current_user.invited_to_system? :contract_manager

UberAdmin

Invitational provides a special, system-wide, invitation and role called :uberadmin. A user that has claimed an UberAdmin invitation will always indicate they have been invited to a given role for a given entity. In other words, every call to invited_to? or invited_to_system? for an UberAdmin will return true.

To create an UberAdmin invitation:

Invitation.invite_uberadmin "foo@bar.com"

As with creating standard invitations, you can pass a user instead of an email to have the invitation claimed immediately by that user:

Invitation.invite_uberadmin current_user

The process to claim an UberAdmin invitation is the same as any other invitation.

To make getting started with a brand new Invitational based environment easier, a rake task is provided to create a new UberAdmin invitation.

rake invitational:create_uberadmin

This will output the claim hash for a new UberAdmin invitation.

You can test to see if the a user is an uberadmin through:

current_user.uberadmin?

CanCanCan

Invitational adds a new condition key to CanCanCan's abilities, :role. This allows you to define the role(s) that a user must be invited into for a specific entity in order to perform the specified action. For example, to indicate that a user invited to a Parent entity in an admin role can manage the parent entity, but a user invited to a staff role can only read the parent entity, in your ability.rb file:

can :manage, Parent, roles: [:admin]
can :read, Parent, roles: [:staff]
cannot :edit, Parent, roles: [:consultant]

Wildcard Roles

Often is is neccessary to indicate that a user invited in any role has a given permission. For example, to indicate that any user invited to a Parent entity can view that Parent, regardless of their role:

can :view, Parent, roles: [:*]

System Roles

To specify system roles for a given ability, utilize the system_roles method inside a roles: array:

can :manage, contract, roles: [system_roles([:contract_manager, :sales_manager])]

Invitation to a parent (or other attribute)

To idenfitify abilities based upon invitations to a parent entity or other attribute, Invitational provides an attribute_roles method. The first argument is symbol indicating the attribute name of the parent entity, the second is an array of roles in which the user must be invited to the parent entity:

can :manage, Child, roles[attribute_roles(:parent, [:admin, :staff])]

To reference invitations on a "grand parent" (or higher) entity, attribute_roles optionally accepts an array as the first parameter, indicating the "path" to the target entity.

To indicate that an invitation to the grandparent found here:

entity = Entity.first

entity.parent.grandparent

Pass the following as the first attribute:

can :manage, Child, roles[attribute_roles([:parent, :grandparent],  [:admin])]

To specify child and parent invitations, you can combine them on one line:

can :manage, Child, roles[:child_admin, attribute_roles(:parent, [:admin, :staff])]

However, it is recommended to specify them on separate lines:

can :manage, Child, roles[:child_admin]
can :manage, Child, roles[attribute_roles(:parent, [:admin, :staff])]