Authorization in Rails 3.1 : CanCan, CanTango, declarative_authorization? - ruby-on-rails

I have looked at declarative_authorization, CanCan, and CanTango. They all are good in adding authorization to the application but I was wondering how does one add authorization to specific instance of a model i.e. a person can have a manage access in one project and only limited (read less than manage: limited update, etc) in another.
Could you please a better way? Apologies if my question sounds too trivial. It could be because I am new to RoR.
thanks,
John

As I know CanCan and declarative_authorization, and I implemented role-based authorizations with both, I recommend CanCan. Just my two cents.
Example (untested, unfortunately I cannot test here and I have no access to my code)
So let's say we have a structure like this:
class User < ActiveRecord::Base
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :users
# attributes: project_read, project_create, project_update
end
Then, CanCan could look like this:
class Ability
include CanCan::Ability
def initialize(user)
#user = user
#role = user.role
# user can see a project if he has project_read => true in his role
can :read, Project if role.project_read?
# same, but with create
can :create, Project if role.project_create?
# can do everything with projects if he is an admin
can :manage, Project if user.admin?
end
end
You can find all information you need in the CanCan wiki on github. Personal recommendation to read:
https://github.com/ryanb/cancan/wiki/Defining-Abilities
https://github.com/ryanb/cancan/wiki/Defining-Abilities-with-Blocks
https://github.com/ryanb/cancan/wiki/Authorizing-Controller-Actions
Basically you just need to extend the example above to include your roles through your relations. To keep it simple, you can also create additional helper methods in ability.rb.
The main mean caveat you may fall for (at least I do): Make sure your user can do something with a model before you define what the user can't. Otherwise you'll sit there frustrated and think "but why? I never wrote the user can't.". Yeah. But you also never explicitly wrote that he can...

class User < ActiveRecord::Base
belongs_to :role
delegate :permissions, :to => :role
def method_missing(method_id, *args)
if match = matches_dynamic_role_check?(method_id)
tokenize_roles(match.captures.first).each do |check|
return true if role.name.downcase == check
end
return false
elsif match = matches_dynamic_perm_check?(method_id)
return true if permissions.find_by_name(match.captures.first)
else
super
end
end
private
def matches_dynamic_perm_check?(method_id)
/^can_([a-zA-Z]\w*)\?$/.match(method_id.to_s)
end
def matches_dynamic_role_check?(method_id)
/^is_an?_([a-zA-Z]\w*)\?$/.match(method_id.to_s)
end
def tokenize_roles(string_to_split)
string_to_split.split(/_or_/)
end
end
Usage:
user.is_an? admin
user.can_delete?

Related

Creating a multi tenant rails app. Each tenant must have an owner and other members. How can I accomplish that?

So I want to build a multi tenant app using Postgres schemas.
People can create a "site" and become its owner. And each site can have many Members.
The owner needs to be able to login to site.app.com/admin to manage his site.
I'm so confused and don't know where to start.
I don't know if I should put the Owner in the public schema or in its Site schema.
Can someone clarify this please.
Thanks
To solve this, you'll need to consider both the server configuration itself (i.e. have one application server and use a catch-all methodology to translate passed subdomains to a 'site identifier' in your application itself), and the application's structure (i.e. have a single application with a number of tables (as you usually would), and store a 'site identifier' in each table to retrieve the correct data for the site).
With regards to simplifying future maintenance and avoiding serious code complexity, I would personally do something like this:
Application Server configuration
Set up an Nginx server with a 'catch-all', which uses the same document root as the application itself.
i.e.
Source: https://www.nginx.com/resources/wiki/start/topics/examples/server_blocks/
server {
listen 80 default_server;
server_name _; # This is just an invalid value which will never trigger on a real hostname.
access_log logs/default.access.log main;
server_name_in_redirect off;
root /var/www/default/htdocs;
}
Rails Application
Add a before_filter to your ApplicationController that retrieves the currently used subdomain, and creates a variable that you can use in queries, etc, like this:
class ApplicationController < ActionController::Base
before_filter :get_current_site_scope
private
def get_current_site_scope
if sesssion[:current_site_scope].nil?
session[:current_site_scope] = request.subdomain
end
# return the existing #current_site_scope variable, if not null
# otherwise, set it to the session value (set above) and return it
#current_site_scope ||= session[:current_site_scope]
end
end
This will store the current subdomain in the current session, under the :current_site_scope key, and will allow you to use #current_site_scope in any controller that inherits from the ApplicationController.
Once you've added a site attribute to each site-related database table, you can then retrieve site related data by modifying each Active Record query to use this. i.e.
articles = Article.find_by(site: #current_site_scope)
This is one of the ways that I would do something like this, but it may not necessarily be the best way.
Before you start jumping into code, however, I would strongly advise that you draw your plan out on paper (or whatever planning method helps you the most), and spend some time considering the architecture and it's constraints/issues. A structure like this could become very messy and confusing very, very quickly, if not thoroughly planned and carefully executed. :)
Good luck!
I'm so confused and don't know where to start.
I'll detail how you should do it (it will be quite abstract)...
Your schema should have accounts and users.
An Account is a "site" (IE has a subdomain), and is the scope around which you'll build your data.
You'll have:
#app/models/account.rb
class Account < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships, > { extending AddRole }
end
#app/models/membership.rb
class Membership < ActiveRecord::Base
belongs_to :account
belongs_to :user
belongs_to :role
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :accounts, through: :memberships
end
#app/models/role.rb
class Role < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
This will set up the Account to have a series of memberships, to which you can add "roles". The roles will give you the ability to authorize different activities - either using CanCanCan or similar.
--
The above will give you the ability to open an account (a "site"), to which you'll then be able to add users with the appropriate role.
The scoping aspect will happen through the account model. I'm not hugely experienced with PGSQL's scoping, but according to the apartment gem, you're able to capture the "account" via the `Subdomain", to which you'll be able to scope your data.
You can capture the Account's subdomain using some constraints in the routes:
#config/routes.rb
scope constraints: AccountSubdomain do
root "application#index"
end
#lib/account_subdomain.rb
module AccountSubdomain
def initializer(router)
#router = router
end
def self.matches?(request)
Account.exists? request.subdomain #-> use "friendly_id" to make "slug" to act as subdomain
end
end
This will only allow you to access: http://account1.url.com but not http://non-account.url.com, passing the subdomain to the apartment gem so your application can scope the data around it.
If you wanted to use the apartment gem, you'd be able to scope around the account; I've not had much experience with that so I cannot comment on specifics.
I can comment on how broad-level scoping works....
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :current_account #-> this would probably be handled by apartment
def index
#users = current_account.users
end
private
def current_account
#current_account ||= Account.find request.subdomain
end
end
Then you'd be able to use:
#app/views/application/index.html.erb
<% #users.each do |user| %>
<%= user.name %>
<%= user.role %>
<% end %>
Finally, if you're wondering what the extending reference is, it's an ActiveRecord Association Extension.
I made my own gem to add "join attributes" to a dependent model. It will allow you to call:
#account = Account.find params[:id]
#account.users.each do
user.role #-> outputs role
end
The code is as follows:
#app/models/concerns/add_role.rb
module AddRole
#Load
def load
roles.each do |role|
proxy_association.target << role
end
end
#Private
private
#Roles
def roles
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({role: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Role
def items
through_collection.map(&:role)
end
#Target
def target_collection
proxy_association.target
end
end

Rails: Using CanCan to assign multiple roles to Users for each organization they belong to?

A User can belong to many Organizations. I would like User to be able to be assigned different roles/authorizations for each of the organization it belongs to.
For example, user "kevin" may belong to organization "stackoverflow" and "facebook." kevin should be able to be an admin for stackoverflow, and a regular member(read+write) for facebook.
However, the CanCan gem only seems to address user roles for a single organization. I'm still a beginner, but from what I can gather, the CanCan gem assumes user roles are tied only to the main app.
How would I be able to assign separate roles for different organizations, preferably using the CanCan gem?
You're thinking that you have to save roles as a string field in the User model. You don't have to, at all:
class User
has_many :roles
end
class Role
belongs_to :user
belongs_to :organization
attr_accessible :level
end
class Ability
def initialize(user)
can :read, Organization
can :manage, Organization do |organization|
user.roles.where(organization_id:organization.id,level:'admin').length > 0
end
can :write, Organization do |organization|
user.roles.where(organization_id:organization.id,level:'member').length > 0
end
end
end
we have something like this. the solution is to override the current_ability method. in your case, you probably have a join table for users and organization. let's call that user_organizations. In this join table, you probably also store the user's role for a particular organization, right? So let's use that table to define the current ability. In your application controller
def current_ability
# assuming you have a current_user and current_organization method
Ability.new UserOrganization.where(user_id: current_user.id, organization_id: current_organization.id).first
end
# ability.rb
class Ability
include CanCan::Ability
def initialize(user_organization)
user_organization ||= UserOrganization.new
case user_organization.role
when 'admin'
when '...'
when nil
# for users not a member of the organization
end
end
end
hope this gives you some idea

Defining abilities in more complex environment with role and group models

in my rails app (I use devise and cancan), each (registered) user belongs to exactly one role ('Administrator' or 'Users') but to at least one group (something like 'Family', 'Friends', 'Co-workers'). At runtime, when a new folder (see below) is created, a habtm relation to one or many groups can be set, which defines who can access the folder. Selecting no group at all should result in a world-wide accessible folder (i.e. users do not have to be logged in to access these folders). But right now, I don't know yet, how to define such world-wide accessible folders in my ability.rb, because I do not know how to define "can read folders which have no groups associated to it".
The relevant snippet of my app/models/ability.rb looks like this:
user ||= User.new
if user.role? :Administrator
can :manage, :all
elsif user.role? :Users
# user should only be able to read folders, whose associated groups they are member of
can :read, Folder, :groups => { :id => user.group_ids }
else
# here goes the world-wide-accessible-folders part, I guess
# but I don't know how to define it:
## can :read, Folder, :groups => { 0 } ???
end
The relevant snippet of my app/controllers/folders_controller.rb looks like this:
class FoldersController < ApplicationController
before_filter :authenticate_user!
load_and_authorize_resource
Can someone give me a hint?
I had the same problem just the other day. I figured out the solution after reading the CanCan readme, which you should do if you haven't yet.
You can view my solution here: Context aware authorization using CanCan
To give you an answer more specific to your use case, do the follow:
In your application controller you need to define some logic which will pick your abilities.
class ApplicationController < ActionController::Base
check_authorization
def current_ability
if <no group selected logic> # Maybe: params[:controller] == 'groups'
#current_ability = NoGroupSelectedAbility.new(current_user)
else
#current_ability = GroupSelectedAbility.new(current_user)
end
end
# Application Controller Logic Below
end
You'll then need to create a new ability (or abilities) in your app/models/ folder. You can also do cool stuff like this:
if request.path_parameters[:controller] == groups
#current_ability = GroupsAbility.new(current_group_relation)
end
Where current_group_relation is defined in app/controllers/groups_controller.rb. This will give you specific abilities for specific controllers. Remember that a parent classes can call methods in child classes in Ruby. You can define a method in your controller, and call it from ApplicationController, as long as you are certain what controller is currently being used to handle the request.
Hope that helps.
EDIT: I wanted to show you what a custom ability looks like.
# File path: app/models/group_ability.rb
class GroupsAbility
include CanCan::Ability
# This can take in whatever you want, you decide what to argument to
# use in your Application Controller
def initialize(group_relation)
group_relation ||= GroupRelation.new
if group_relation.id.nil?
# User does not have a relation to a group
can :read, all
elsif group_relation.moderator?
# Allow a mod to manage all group relations associated with the mod's group.
can :manage, :all, :id => group_relation.group.id
end
end
end

Any lazy way to do authentication on RoR?

I want to make a simple login, logout, also, different user have different user role. The Restful authentication seems works great, and the cancan is also very sweet for controlling user ability. But the question is how can I let these two works together. I watched the railcast, I was whether how to detect the user ability? Do I need to add a "ability" column in the user table?? Thank u.
http://railscasts.com/episodes/67-restful-authentication
http://railscasts.com/episodes/192-authorization-with-cancan
Look at the CanCan GitHub page: http://github.com/ryanb/cancan
Based on looking at both that and the RailsCast, I notice two things:
You define Ability as a separate model. There doesn't appear to be any necessary database columns.
There is no way you are forced to do roles, you are free to do this however you will.
With restful_authentication, just do the normal thing with your User model.
The most natural way to add CanCan would be to add an extra column to your User model called role or ability or something, then define methods as you see fit. Personally I'd probably do some kind of number system stored in the database, such as "0" for admin, "1" for high-level user, "2" for low-level user, etc.
Here's a few possibilities:
# Returns true if User is an admin
def admin?
self.role == 0
end
And:
# Returns true if User is admin and role?(:admin) is called, etc.
def role?(to_match)
{
0 => :admin,
1 => :super_user,
2 => :user,
3 => :commenter,
}[self.role] == to_match
end
Then in your Ability initialize method, you can use some kind of conditionals to set abilities, such as these snippets from the Railscast/readme:
if user.role? :admin
can :manage, :all
elsif user.role? :super_user
...
end
Or:
if user.admin?
can :manage, :all
else
...
end
I wrote a simple solution that works with CanCan too, just add a role_id:integer column to the User model:
# puts this in /lib/
module RolePlay
module PluginMethods
def has_roleplay(roles = {})
##roles = roles
##roles_ids = roles.invert
def roles
##roles
end
def find_by_role(role_name, *args)
find(:all, :conditions => { :role_id => ##roles[role_name]}, *args)
end
define_method 'role?' do |r|
r == ##roles_ids[role_id]
end
define_method :role do
##roles_ids[role_id]
end
end
end
end
then include this line in config/initializers/roleplay.rb
ActiveRecord::Base.extend RolePlay::PluginMethods
finally use it in your User model:
class User < ActiveRecord::Base
# ...
has_roleplay(:admin => 0, :teacher => 1, :student => 2)
# ...
end
now your model will have 2 new methods:
#user.role?(:admin) # true if user has admin role
#user.role # returns role name for the user

How to test a named_scope that references a class attribute with Shoulda?

I have the following ActiveRecord classes:
class User < ActiveRecord::Base
cattr_accessor :current_user
has_many :batch_records
end
class BatchRecord < ActiveRecord::Base
belongs_to :user
named_scope :current_user, lambda {
{ :conditions => { :user_id => User.current_user && User.current_user.id } }
}
end
and I'm trying to test the named_scope :current_user using Shoulda but the following does not work.
class BatchRecordTest < ActiveSupport::TestCase
setup do
User.current_user = Factory(:user)
end
should_have_named_scope :current_user,
:conditions => { :assigned_to_id => User.current_user }
end
The reason it doesn't work is because the call to User.current_user in the should_have_named_scope method is being evaluated when the class is being defined and I'm change the value of current_user afterwards in the setup block when running the test.
Here is what I did come up with to test this named_scope:
class BatchRecordTest < ActiveSupport::TestCase
context "with User.current_user set" do
setup do
mock_user = flexmock('user', :id => 1)
flexmock(User).should_receive(:current_user).and_return(mock_user)
end
should_have_named_scope :current_user,
:conditions => { :assigned_to_id => 1 }
end
end
So how would you test this using Shoulda?
I think you are going about this the wrong way. Firstly, why do you need to use a named scope? Wont this just do?
class BatchRecord < ActiveRecord::Base
belongs_to :user
def current_user
self.user.class.current_user
end
end
In which case it would be trivial to test. BUT! WTF are you defining current_user as a class attribute? Now that Rails 2.2 is "threadsafe" what would happen if you were running your app in two seperate threads? One user would login, setting the current_user for ALL User instances. Now another user with admin privileges logs in and current_user is switched to their instance. When the first user goes to the next page he/she will have access to the other persons account with their admin privileges! Shock! Horror!
What I reccomend doing in this case is to either making a new controller method current_user which returns the current user's User instance. You can also go one step further and create a wrapper model like:
class CurrentUser
attr_reader :user, :session
def initialize(user, session)
#user, #session = user, session
end
def authenticated?
...
end
def method_missing(*args)
user.send(*args) if authenticated?
end
end
Oh, and by the way, now I look at your question again perhaps one of the reasons it isn't working is that the line User.current_user && User.current_user.id will return a boolean, rather than the Integer you want it to. EDIT I'm an idiot.
Named scope is really the absolutely wrong way of doing this. Named scope is meant to return collections, rather than individual records (which is another reason this fails). It is also making an unnecessary call the the DB resulting in a query that you don't need.
I just realized the answer is staring right at me. I should be working from the other side of the association which would be current_user.batch_records. Then I simply test the named_scope on the User model and everything is fine.
#Chris Lloyd - Regarding the thread safety issue, the current_user attribute is being set by a before_filter in my ApplicationController, so it is modified per request. I understand that there is still the potential for disaster if I chose to run in a multi-threaded environment (which is currently not the case). That solution I suppose would be another topic entirely.

Resources