Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First public commit

  • Loading branch information...
commit 613026699bfe5095b6686f4d03870201e290c025 0 parents
@milann authored
Showing with 1,691 additions and 0 deletions.
  1. +170 −0 CHANGELOG.txt
  2. +20 −0 MIT-LICENSE
  3. +181 −0 README.rdoc
  4. +35 −0 README_developers.txt
  5. +22 −0 Rakefile
  6. +13 −0 UPGRADE.rdoc
  7. +8 −0 about.yml
  8. BIN  doc/authorization_example.gif
  9. +38 −0 doc/authorization_example.rb
  10. +36 −0 generators/role_model/role_model_generator.rb
  11. +5 −0 generators/role_model/templates/fixtures.yml
  12. +21 −0 generators/role_model/templates/migration.rb
  13. +9 −0 generators/role_model/templates/role_model.rb
  14. +5 −0 generators/role_model/templates/role_user_model.rb
  15. +10 −0 generators/role_model/templates/unit_test.rb
  16. +51 −0 init.rb
  17. +2 −0  install.rb
  18. +10 −0 lib/authorizable_by_default.rb
  19. +152 −0 lib/authorization.rb
  20. +152 −0 lib/authorized_context.rb
  21. +71 −0 lib/computed_roles.rb
  22. +43 −0 lib/publishare/exceptions.rb
  23. +82 −0 lib/publishare/hardwired_roles.rb
  24. +135 −0 lib/publishare/identity.rb
  25. +203 −0 lib/publishare/object_roles_table.rb
  26. +210 −0 lib/publishare/parser.rb
  27. +4 −0 tasks/authorization_tasks.rake
  28. +3 −0  test/README.txt
170 CHANGELOG.txt
@@ -0,0 +1,170 @@
+TO DO
++ Add Right model generator and DB-backed way of handling rights in addition to inlined "permit" checks
++ Added namespacing to @options instance variable to prevent possible name clashes
++ Add test generator instead of handling tests in test apps
++ Add support for groups
++ Extend grammar to allow "(admin or moderator or some_role) of some_model" (?) [Chris Hapgood]
++ Extend coverage to models. Look at Bruce Perens's ModelSecurity and access with_scope. (9/3006 - Recently investigated extension to model and the most programmer-friendly DSLs may require too much hacking on ActiveRecord.)
+
+
+CHANGES (from most recent to oldest)
+
+
+=== 1.0.10 release (February 27, 2008)
+
+* Patch Series : Granular redirection configuration submitted by Thomas Weibel
+
+ WARNING : If you are upgrading from a previous install you may need
+ to change some configuration settings in your environment.rb file.
+
+ Remove DEFAULT_REDIRECTION_HASH config
+ Added granular LOGIN_REQUIRED_REDIRECTION hash or path config
+ Added granular PERMISSION_DENIED_REDIRECTION hash or path config
+ Added STORE_LOCATION_METHOD config
+ Support custom flash messages for each redirection type
+ Updated README.txt to provide instructions.
+ Enhanced support for integration with restful_authentication plugin.
+
+=== 1.0.9 release (February 26, 2008)
+
+* Patch #8571 : Add type argument to is_role_of_what submitted by Aslak Hellesøy (aslak_hellesoy)
+
+ In my RESTful index views for an AR type I often want to list all of the records *for a given type* for which the current
+ user has the role "show". (As opposed to getting *any* record for which the user has the role)
+
+ In order to achieve this, I have patched identity.rb so tht I can do this:
+
+ def index
+ if current_user.permit? 'admin'
+ # show all projects
+ @projects = Project.find(:all)
+ else
+ @projects = current_user.is_show_for_what(Project)
+ end
+ end
+
+=== 1.0.8 release (February 26, 2008)
+
+* Patch #11352 : Fixes a bug with role_regex and simple quoted roles submitted by 'a French RoR developer'
+
+ Documentation says:
+
+ <role> ::= /\w+/ | /'.*'/
+
+ But the next permission string isn't well parsed: " 'abcd:efgh' or 'abcd:ijkl' "
+ You get an error because the role_regex defined in parser.rb eats every simple quote between the first and the last
+ simple quote in the string.
+
+ So i patched the two instances of role_regex in parser.rb, from this:
+ role_regex = '\s*(\'\s*(.+)\s*\'|([A-Za-z]\w*))\s*'
+
+ to this (the question mark ends the first pattern as soon as possible, avoiding the inner simple quotes to be eaten):
+ role_regex = '\s*(\'\s*(.+?)\s*\'|([A-Za-z]\w*))\s*'
+
+=== 1.0.7 release (February 25, 2008)
+
+* Patch #9431 : Fixes a bug in identity.rb submitted by Michel Martens (blaumag)
+
+ If some authorizable instance accepts a role, then it responds true when queried for has_[role_name]?
+
+ Example:
+ country.has_kings? #=> false
+
+ user.has_role "king", country
+ country.has_kings? #=> true
+
+ user.has_no_role "king", country
+ country.has_kings? #=> true
+
+ The last time, country.has_kings? should be false.
+
+=== 1.0.6 release (February 25, 2008)
+
+* Patch #12170 : Additional HABTM options for acts_as_authorized_user
+ A very simple patch that allows options to be passed to the has_and_belogs_to_many relationship. This seems necessary
+ if the "User" object has a different name from the table name. has_and_belong_to_many does not automatically
+ use the table set by the "User" object so it must be specified (along with the foreign key if applicable).
+
+ Patch submitted by Eric Anderson (eric1234)
+
+=== 1.0.5 release (February 25, 2008)
+
+* Feature : Add additional test for current_user being set to the symbol ':false'.
+ This is for compatibility with the restful_authentication plugin which will
+ set current_user to :false on a bad login. Previously we were only testing
+ for current_user.nil? which was incomplete.
+
+=== 1.0.4 release (February 25, 2008)
+
+* Bugfix : RubyForge bug #9368. Problems with about.yml
+ Fixes a minor bug in the about.yml plugin metadata file
+ so that it will parse cleanly. [GR]
+
+=== 1.0.3 release (February 17, 2008)
+
+* Minor changes to USAGE text for ./script/generate role_model
+
+=== 1.0.2 release (February 17, 2008)
+
+* From this release forward the plugin requires use of Ruby on Rails version 2.x. Version 1.0.1 is the final release fully compatible with Rails 1.2.x.
+* Upgraded the database migration generator to create the new Rails 2.0.x style 'sexy migrations'.
+
+=== 1.0.1 release (February 17, 2008)
+
+* Moved source code to public Git repository at GitHub.com (http://github.com/DocSavage/rails-authorization-plugin/tree/master)
+* Removed attr_protected declaration from acts_as_authorized_user, acts_as_authorizable methods. These conflicted with usage of the Authorization plugin with models generated by the restful_authentication generator or any model that specified the safer attr_accessible whitelist. RA encourages the safer attr_accessible whitelisting of attributes that are accessible from its models. You cannot apply both attr_accessible and attr_protected in the same model. Users are encouraged to specify a whitelist of attr_accessible model attributes for their applications security. [grempe]
+
+=== SVN
+
+* Performance improvement for has_role? [Sean Geoghegan]
+
+* Allow customization of message on redirection after failed authorization (:redirect_message option) [Joey Geiger]
+
+* Patch to allow authorizable objects that use single table inheritance (STI) [Sean Geoghegan]
+
+=== 1.0 release (Sept 13, 2006)
+
+* Added attr_protected for habtm and has_many role ids to block security concern if developers use update_attributes(params[:auth_obj]) on an authorizable object [Michael Schuerig]
+
+* Use before_filter rather than prepend_before_filter so necessary instance variables (and methods) can be established before trying authorization checks. This fix came about for Mephisto blog where a class-level permit "admin of site" was used. The site attribute was set in a before_filter. If you prepend your authorization filter, it will execute before any other before_filter, which is probably not a good idea.
+
+* Add "about" yaml for future Rails plugin directory.
+
+* Cleaned up exception handling a little [due to suggestion by Michael Schuerig]
+
+* Add generator for role model and migration, e.g., "script/generate role_model Role".
+ Role model must be called "Role" at this time. More general naming as a TO DO.
+
+* Removed simple_roles_table to simplify plugin.
+
+* Moved all files in Authorization namespace into /publishare subdirectory
+ to reduce danger of clashes in load path [nod to Michael Schuerig].
+
+* Small code refinement patch [Michael Schuerig]
+
+* The colon preceding a model name in the authorization expression is now optional. The parser uses accepted prepositions to disambiguate models from roles.
+
+* Change default parser from Recursive Descent parser to Eval parser.
+Currently implemented recursive descent parser doesn't handle left-sided
+boolean expressions well. Eval parser relies on Ruby (good thing), but
+wherever there's an eval, we have to be more careful.
+
+* Will start linking to and monitoring forum area at RubyForge
+http://rubyforge.org/forum/?group_id=1797
+
+* Added changelog :)
+
+* Added return false to handle_redirection to short-circuit filters if
+redirect occurs. This is second fix to prevent double renders.
+
+* Changed the requires to pull files from the plugin directory. (Necessary for name conflicts between plugin and apps)
+
+* Minor fixes to update documentation
+
+=== 1.0 rc3 (July 19, 2006)
+
+* Fix to prevent double redirect
+
+* Fix to migration examples
+
+... see svn log
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2006 William T Katz
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
181 README.rdoc
@@ -0,0 +1,181 @@
+= Guardian
+
+Guardian is an authorization plugin for Rails build on top of the powerful rails-authorization-plugin[http://github.com/DocSavage/rails-authorization-plugin].
+It is to rails-authorization-plugin as Ubuntu is to Debian - a more user-friendly distribution.
+
+The main features are:
+* declarative role definition in the authorized user model as well as database based role definition
+* declarative permissions definition at one place (per controller) using a simple and very readable micro DSL
+* uses a concept of an authorized context for declaratively marking the parts of your code that should be checked for permissions
+* really fine-grained access control (you are not restricted to controller actions access control only but can go further)
+* easy to start with
+* versatile in its possibilities (courtesy of rails-authorization-plugin[http://github.com/DocSavage/rails-authorization-plugin].)
+
+Some of the other improvements or additions to the rails-authorization-plugin:
+* a more standard way of defining the configuration constants
+* added a concept of superuser - a role that has a universal access to all context without explicitly declaring it
+* on_access_denied event handler for controllers
+* on_login_required event handler for controllers
+* shorter name & shorter installation and usage instructions :)
+
+WARNING: I'm not going to cover all the possibilities of this thing here. If you think you are familiar with the basics I describe here,
+jump right to the rails-authorization-plugin home page and read the stuff that's written there (mainly the parts about the authorized user API and the authorizable models API).
+
+== Assumptions
+
+Your controllers have to implement the current_user method.
+This should be no problem with the Rails authentication systems used these days.
+
+== Installation
+
+First, install the plugin:
+
+ ./script/plugin install git://github.com/milan-novota/guardian.git
+
+Add this to your <code>/config/preinitializer.rb</code> file (create it if it doesn't exist):
+
+ USE_ROLES_TABLE = true # false if you don't plan to use the database table for saving the roles
+ AUTHORIZABLE_BY_DEFAULT = true # false if you don't want all the models to be authorizable by default
+
+Create file <code>config/initializers/authorization.rb</code>, add these lines and set the constants to whatever you think seems OK:
+
+ SUPERUSER_ROLE = 'admin' # this role will have full acces in any authorized context without explicitly defining it
+ LOGIN_REQUIRED_REDIRECTION = '/login'
+ LOGIN_REQUIRED_MESSAGE = 'Log in first'
+ PERMISSION_DENIED_REDIRECTION = ''
+ PERMISSION_DENIED_MESSAGE = "You are not cool enough to do this."
+ STORE_LOCATION_METHOD = :store_location # how to store actual location before we redirect after login required event
+
+If you plan to use the database for persisting the roles and users to roles relationships (recommended):
+
+ ./script/generate role_model Role
+ rake db:migrate
+
+Add this to your User model:
+
+ acts_as_authorized_user
+
+That's it!
+
+== Usage
+
+Just to make things clear - as you probably know, there are three main parts to every authorization system - users, roles and permissions, which consists
+of a user in some role and a context (in which this user is allowed to operate). Guardian is not different in this.
+
+Users are easy (you should have your User model ready by now). Let's start with roles.
+
+=== Roles
+
+Role can be based on virtually any condition that you think should affect the fact, that at some moment in time some users have access to some feature of the system and some don't. And when I say any, I mean any:
+
+ class Users
+ acts_as_authorized_user
+ has_role 'lucky bastard', :if => lambda { rand == 0.32456112353 } # really ephemeric role - changes randomly no matter of conditions
+ has_role 'self of user', :if => lambda {|u1, u2| u1.id == u2.id } # ehternal role - user will occupy it as long as she exists
+ has_role 'author of post',:if => lambda {|u,p| p.author == u } # user has this role for the time the post exists (or he makes up his mind about the authorship of the post)
+ has_role 'adult', :if => lambda {|u| u.adult? }
+
+ def adult?
+ age > 18
+ end
+
+ ...
+
+ end
+
+That was a role definition by function. Another way of assigning a role to a user is by doing it in a database via a set of methods provided by r-a-p:
+
+ user.has_role "admin"
+ user.has_role "manager of", user2
+
+You can query your objects whether they have some role or they are in a specific relationships with other objects:
+
+ user.has_role? "admin"
+ user.has_role? "manager of", user2
+ user2.accepts_role "manager", user
+
+Actually, you can do much more, just check out the r-a-p documentation.
+
+=== Permissions - Contexts
+
+Let's say you are about to build a mini app which will serve as your online diary. Your permissions definition could look like this:
+
+ class DiaryContoller << ApplicationController
+
+ grant do
+ grant "author of post"
+ can "update post"
+
+ grant "self of user or friend of user"
+ can "get all user's posts"
+
+ grant "author of post or 'lucky bastard'"
+ can "get post"
+ end
+
+ def index
+ @user = params[:user]
+ authorized_context "get all users's posts" do
+ @posts = @user.posts
+ end
+ end
+
+ def show
+ @post = Post.find(params[:id])
+ authorized_context "get post"
+ end
+
+ def edit
+ @post = Post.find(params[:id])
+ authorized_context "update post"
+ end
+
+ def update
+ @post = Post.find(params[:id])
+ authorized_context "update post" do
+ ...
+ end
+ end
+
+ end
+
+Use of authorized_context in every method might seem quite chatty for some people, I know. However, you don't need to use the authorized_context declaration if you really don't need to.
+You can easily identify these contexts with particular actions.
+
+ grant do
+ grant "author of post"
+ can "update post" => [:edit, :update]
+
+ grant "self of user or friend of user"
+ can "get all user's posts"
+
+ grant "author of post or 'lucky bastard'"
+ can "get post" => :show
+ end
+
+When using this form of permission declaration, the contexts are checked in a before_filter, which means you need to set up the instance variables needed for authorization before these are evaluated.
+
+If you want to know more, check the r-a-p documentation.
+
+=== Ideological background
+
+The basic concept of authorization, as I understand it, is a role. Role can express various things:
+
+ 1. relation of a user to the system as a whole (eg. to be an admin of the system)
+ 2. relation of a user to some kind of entities (eg. to be a moderator of comments)
+ 3. relation of a user to some particular entity (eg. to be an owner of some resource)
+ 4. some other complex relation (eg. to be a friend of a user that is a owner of some resource)
+ 5. that user has some attribute(s) or it responds to some message in some particular way (eg. to be a teenager)
+
+A really fine grained authorization system should allow you to define role for a user based on any of the above mentioned criteria. Furthermore, it should allow you to set more than one role for a user. (The simplest forms of authorization plugins for Rails usually allow you define just the first kind of roles and set just one role for a user.)
+
+
+The other part of authoriation is a mechanism that decides which part of code to run (or not to run) based on the fact if a user fits into some role (set of roles) or not. To apply this mechanism, we have to find the points where the authorization should take place and select roles for which the code should or should not be run.
+
+The way that works for me in Rails is to define roles on the model level and to leave authorization mechanism (setting allowed roles for parts of code that I want to be authorized and asking if current user has the role that is permitted to run the part) entirely for controllers/views.
+
+For this I use this plugin. rails-authorization-plugin has all the possibilities I just mentioned built right into it (various kinds of roles, many roles for one user, authorization on controller and view level). My wrapper on top of it provides me with some more conveniences such as authorized contexts and computed roles.
+
+=== Disclaimer
+
+This plugin is a work in progress and I don't recommend to use it in any circumstances.
35 README_developers.txt
@@ -0,0 +1,35 @@
+
+INSTRUCTIONS FOR DEVELOPERS:
+
+Authorization and Git
+
+All source code for the plugin is managed in a Git master repository. Currently this repository is hosted on GitHub, which is a great site that makes working with, and sharing, Git managed code so much better.
+
+You can browse the master Git repo here:
+
+http://github.com/DocSavage/rails-authorization-plugin/tree/master
+
+Authorization @ GitHub
+
+If you want to learn more about how you can use GitHub to create your own fork of the Authorization repository and use that as the base for your enhancements this excellent article provides a great start:
+
+http://railsontherun.com/2008/3/3/how-to-use-github-and-submit-a-patch
+
+
+Testing
+
+We request that all patches be fully tested prior to submission and we would like all code changes to be accompanied wherever possible by valid passing tests. You can test the application by downloading our most recent test repository from Git and running the tests as instructed in the README. Please submit a separate patch against the test repo to accompany any plugin change patches.
+
+http://github.com/grempe/rails-authorization-plugin-test/tree/master
+
+Instructions for using the test app are available:
+
+http://github.com/grempe/rails-authorization-plugin-test/tree/master/README
+
+We also welcome any patches that would integrate a plugin testing framework (RSpec) into the plugin itself so we could use the test app only for demo purposes and be able to run the suite of tests directly in the plugin code base.
+
+Submitting Patches
+
+The recommended way to submit patches is to initiate a pull request from a Git fork @ GitHub.
+
+However, we will also accept patches submitted on the Authorization Google Group, or by email.
22 Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the authorization plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the authorization plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Authorization'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README.txt')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
13 UPGRADE.rdoc
@@ -0,0 +1,13 @@
+= Instructions for Upgrading
+
+Scroll down to the last time you updated the plugin, then follow the upgrade steps upward till you get back to this message.
+
+== 25/12/08 - Changes has_and_belongs_to_many to a has_many :through association
+
+* Copy generators/role_model/templates/role_model.rb to app/models/role.rb
+* Copy generators/role_model/templates/role_user_model.rb to app/models/role_user.rb
+* Open each file and replace the erb code (<%= %>) with the correct model class name below
+
+ role.rb -> Role
+
+ role_user.rb -> RoleUser
8 about.yml
@@ -0,0 +1,8 @@
+author: Bill Katz
+summary: Adds a flexible mechanism for authorization.
+description: "Adds a flexible mechanism for authorization. Differs from other authorization systems in the following ways: (1) You can specify roles programmatically with model code or use a mixin to keep roles in a database. (2) The plugin uses a clean language for specifying authorization expressions. (3) Ability to handle roles on instances of a model. (4) Rights are explicitly declared in controller and view code. (5) Different levels of authorization complexity are provided through mixins available with the plugin. If you don't want to use the database for authorization, you mixin a HardwiredRoles module. If you want full database support for roles on model instances, you mixin the ObjectRolesTable module."
+homepage: http://www.writertopia.com/developers/authorization
+plugin: http://svn.writertopia.com/svn/plugins/authorization/
+license: MIT
+version: 1.0.10
+rails_version: 2.0+
BIN  doc/authorization_example.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 doc/authorization_example.rb
@@ -0,0 +1,38 @@
+class MeetingController < ApplicationController
+
+ permit "rubyists and wanna_be_rubyists", :except => :public_page
+
+ def public_page
+ render :text => "We're all in Chicago"
+ end
+
+ def secret_info
+ permit "(matz or dhh) and interested in Answers" do
+ render :text => "The Answer = 42"
+ end
+ end
+
+ def find_apprentice
+ @founder = User.find_by_name('matz')
+ permit "'inner circle' of :founder" do
+ if request.post?
+ apprentice = User.find_by_skillset(params[:uber_hacker])
+ ruby_community = Group.find_by_name('Ruby')
+ ruby_community.accepts_role 'yarv_builder', apprentice
+ end
+ end
+ end
+
+ def rails_conf
+ @meeting = Meeting.find_by_name('RailsConf')
+ permit "attendees of :meeting or swedish_mensa_supermodels" do
+ venue = Hotel.find_by_name("Wyndham O'Hare")
+ current_user.is_traveller_to venue
+ if permit? "traveller to :venue and not speaker"
+ Partay.all_night_long
+ @misdeeds = current_user.is_participant_in_what
+ end
+ end
+ end
+
+end
36 generators/role_model/role_model_generator.rb
@@ -0,0 +1,36 @@
+# Shamelessly derived from Rick Olsen's acts_as_attachment
+class RoleModelGenerator < Rails::Generator::NamedBase
+ default_options :skip_migration => false
+
+ def manifest
+ record do |m|
+ # Check for class naming collisions.
+ m.class_collisions class_path, class_name, "#{class_name}Test"
+
+ # Model, test, and fixture directories.
+ m.directory File.join('app/models', class_path)
+ m.directory File.join('test/unit', class_path)
+ m.directory File.join('test/fixtures', class_path)
+
+ # Model class, unit test, and fixtures.
+ m.template 'role_model.rb', File.join('app/models', class_path, "#{file_name}.rb")
+ m.template 'role_user_model.rb', File.join('app/models', class_path, "#{(file_name < 'user') ? "#{file_name.pluralize}_user" : "users_#{file_name}"}.rb")
+ m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb")
+ m.template 'fixtures.yml', File.join('test/fixtures', class_path, "#{table_name}.yml")
+
+ unless options[:skip_migration]
+ m.migration_template 'migration.rb', 'db/migrate', :assigns => {
+ :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}"
+ }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
+ end
+ end
+ end
+
+ protected
+ def add_options!(opt)
+ opt.separator ''
+ opt.separator 'Options:'
+ opt.on("--skip-migration",
+ "Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
+ end
+end
5 generators/role_model/templates/fixtures.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
21 generators/role_model/templates/migration.rb
@@ -0,0 +1,21 @@
+class <%= migration_name %> < ActiveRecord::Migration
+
+ def self.up
+ create_table :<%= (table_name < 'users') ? "#{table_name}_users" : "users_#{table_name}" %>, :id => false, :force => true do |t|
+ t.integer :user_id, :<%= singular_name %>_id
+ t.timestamps
+ end
+
+ create_table :<%= table_name %>, :force => true do |t|
+ t.string :name, :authorizable_type, :limit => 40
+ t.integer :authorizable_id
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :<%= table_name %>
+ drop_table :<%= (table_name < 'users') ? "#{table_name}_users" : "users_#{table_name}" %>
+ end
+
+end
9 generators/role_model/templates/role_model.rb
@@ -0,0 +1,9 @@
+# Defines named roles for users that may be applied to
+# objects in a polymorphic fashion. For example, you could create a role
+# "moderator" for an instance of a model (i.e., an object), a model class,
+# or without any specification at all.
+class <%= class_name %> < ActiveRecord::Base
+ has_many :roles_users, :dependent => :delete_all
+ has_many :users, :through => :roles_users
+ belongs_to :authorizable, :polymorphic => true
+end
5 generators/role_model/templates/role_user_model.rb
@@ -0,0 +1,5 @@
+# The table that links roles with users (generally named RoleUser.rb)
+class <%= (class_name < 'User') ? "#{class_name.pluralize}User" : "Users#{class_name}" %> < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :role
+end
10 generators/role_model/templates/unit_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+ fixtures :<%= table_name %>
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
51 init.rb
@@ -0,0 +1,51 @@
+require File.dirname(__FILE__) + '/lib/authorization'
+require File.dirname(__FILE__) + '/lib/authorized_context'
+require File.dirname(__FILE__) + '/lib/computed_roles'
+require File.dirname(__FILE__) + '/lib/authorizable_by_default'
+
+ActionController::Base.send( :include, Authorization::Base )
+ActionView::Base.send( :include, Authorization::Base::ControllerInstanceMethods )
+
+ActionController::Base.send(:include, Authorization::AuthorizedContext)
+ActionView::Base.send(:include, Authorization::AuthorizedContext::InstanceMethods)
+
+# You can perform authorization at varying degrees of complexity.
+# Choose a style of authorization below (see README.txt) and the appropriate
+# mixin will be used for your app.
+
+# When used with the auth_test app, we define this in config/environment.rb
+# AUTHORIZATION_MIXIN = "hardwired"
+if not Object.constants.include? "AUTHORIZATION_MIXIN"
+ if not (Object.constants.include?("USE_ROLES_TABLE") && !USE_ROLES_TABLE)
+ AUTHORIZATION_MIXIN = "object roles"
+ else
+ AUTHORIZATION_MIXIN = "hardwired"
+ end
+end
+
+if not Object.constants.include? "AUTHORIZABLE_BY_DEFAULT"
+ AUTHORIZABLE_BY_DEFAULT = true
+end
+
+case AUTHORIZATION_MIXIN
+ when "hardwired"
+ require File.dirname(__FILE__) + '/lib/publishare/hardwired_roles'
+ ActiveRecord::Base.send( :include,
+ Authorization::HardwiredRoles::UserExtensions,
+ Authorization::HardwiredRoles::ModelExtensions
+ )
+ when "object roles"
+ require File.dirname(__FILE__) + '/lib/publishare/object_roles_table'
+ ActiveRecord::Base.send( :include,
+ Authorization::ObjectRolesTable::UserExtensions,
+ Authorization::ObjectRolesTable::ModelExtensions
+ )
+end
+
+ActiveRecord::Base.send(:include, Authorization::Base::ComputedRoles)
+
+if AUTHORIZABLE_BY_DEFAULT
+ ActiveRecord::Base.send(:include, Authorization::AuthorizableByDefault)
+end
+
+
2  install.rb
@@ -0,0 +1,2 @@
+puts IO.read(File.join(File.dirname(__FILE__), 'README.rdoc'))
+
10 lib/authorizable_by_default.rb
@@ -0,0 +1,10 @@
+module Authorization
+ module AuthorizableByDefault
+ def self.included(base)
+ base.class_eval do
+ acts_as_authorizable
+ alias :authorized_users :users
+ end
+ end
+ end
+end
152 lib/authorization.rb
@@ -0,0 +1,152 @@
+require File.dirname(__FILE__) + '/publishare/exceptions'
+require File.dirname(__FILE__) + '/publishare/parser'
+
+module Authorization
+ module Base
+
+ # Modify these constants in your environment.rb to tailor the plugin to
+ # your authentication system
+ if not Object.constants.include? "LOGIN_REQUIRED_REDIRECTION"
+ LOGIN_REQUIRED_REDIRECTION = {
+ :controller => 'session',
+ :action => 'new'
+ }
+ end
+ if not Object.constants.include? "PERMISSION_DENIED_REDIRECTION"
+ PERMISSION_DENIED_REDIRECTION = ''
+ end
+ if not Object.constants.include? "STORE_LOCATION_METHOD"
+ STORE_LOCATION_METHOD = :store_location
+ end
+
+ def self.included( recipient )
+ recipient.extend( ControllerClassMethods )
+ recipient.class_eval do
+ include ControllerInstanceMethods
+ end
+ end
+
+ module ControllerClassMethods
+
+ # Allow class-level authorization check.
+ # permit is used in a before_filter fashion and passes arguments to the before_filter.
+ def permit( authorization_expression, *args )
+ filter_keys = [ :only, :except ]
+ filter_args, eval_args = {}, {}
+ if args.last.is_a? Hash
+ filter_args.merge!( args.last.reject {|k,v| not filter_keys.include? k } )
+ eval_args.merge!( args.last.reject {|k,v| filter_keys.include? k } )
+ end
+ before_filter( filter_args ) do |controller|
+ controller.permit( authorization_expression, eval_args )
+ end
+ end
+ end
+
+ module ControllerInstanceMethods
+ include Authorization::Base::EvalParser # RecursiveDescentParser is another option
+
+ # Permit? turns off redirection by default and takes no blocks
+ def permit?( authorization_expression, *args )
+ @options = { :allow_guests => false, :redirect => false }
+ @options.merge!( args.last.is_a?( Hash ) ? args.last : {} )
+
+ has_permission?( authorization_expression)
+ end
+
+ # Allow method-level authorization checks.
+ # permit (without a question mark ending) calls redirect on denial by default.
+ # Specify :redirect => false to turn off redirection.
+ def permit( authorization_expression, *args )
+ @options = { :allow_guests => false, :redirect => true }
+ @options.merge!( args.last.is_a?( Hash ) ? args.last : {} )
+
+ if has_permission?( authorization_expression)
+ yield if block_given?
+ elsif @options[:redirect]
+ handle_redirection
+ end
+ end
+
+ private
+
+ def has_permission?( authorization_expression )
+ @current_user = get_user
+ if not @options[:allow_guests]
+ # We aren't logged in, or an exception has already been raised.
+ # Test for both nil and :false symbol as restful_authentication plugin
+ # will set current user to ':false' on a failed login (patch by Ho-Sheng Hsiao).
+ # Latest incarnations of restful_authentication plugin set current user to false.
+ if @current_user.nil? || @current_user == :false || @current_user == false
+ return false
+ elsif not @current_user.respond_to? :id
+ raise( UserDoesntImplementID, "User doesn't implement #id")
+ elsif not @current_user.respond_to? :has_role?
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" )
+ end
+ end
+ parse_authorization_expression( authorization_expression)
+ end
+
+ # Handle redirection within permit if authorization is denied.
+ def handle_redirection
+ return if not self.respond_to?( :redirect_to )
+
+ # Store url in session for return if this is available from
+ # authentication
+ send( STORE_LOCATION_METHOD ) if respond_to? STORE_LOCATION_METHOD
+ if @current_user && @current_user != :false
+ flash[:notice] = @options[:permission_denied_message] || "Permission denied. You cannot access the requested page."
+ redirect_to @options[:permission_denied_redirection] || PERMISSION_DENIED_REDIRECTION
+ else
+ flash[:notice] = @options[:login_required_message] || "Login is required to access the requested page."
+ redirect_to @options[:login_required_redirection] || LOGIN_REQUIRED_REDIRECTION
+ end
+ false # Want to short-circuit the filters
+ end
+
+ # Try to find current user by checking options hash and instance method in that order.
+ def get_user
+ if @options[:user]
+ @options[:user]
+ elsif @options[:get_user_method]
+ send( @options[:get_user_method] )
+ elsif self.respond_to? :current_user
+ current_user
+ elsif not @options[:allow_guests]
+ raise( CannotObtainUserObject, "Couldn't find #current_user or @user, and nothing appropriate found in hash" )
+ end
+ end
+
+ # Try to find a model to query for permissions
+ def get_model( str )
+ if str =~ /\s*([A-Z]+\w*)\s*/
+ # Handle model class
+ begin
+ Module.const_get( str )
+ rescue
+ raise CannotObtainModelClass, "Couldn't find model class: #{str}"
+ end
+ elsif str =~ /\s*:*(\w+)\s*/
+ # Handle model instances
+ model_name = $1
+ model_symbol = model_name.to_sym
+ if @options[model_symbol]
+ @options[model_symbol]
+ elsif instance_variables.include?( '@'+model_name )
+ instance_variable_get( '@'+model_name )
+ elsif respond_to?(model_symbol)
+ send(model_symbol)
+ # Note -- while the following code makes autodiscovery more convenient, it's a little too much side effect & security question
+ # elsif self.params[:id]
+ # eval_str = model_name.camelize + ".find(#{self.params[:id]})"
+ # eval eval_str
+ else
+ raise CannotObtainModelObject, "Couldn't find model (#{str}) in hash or as an instance variable"
+ end
+ end
+ end
+ end
+
+ end
+end
152 lib/authorized_context.rb
@@ -0,0 +1,152 @@
+module Authorization
+ module AuthorizedContext
+
+ if not Object.constants.include? "SUPERUSER_ROLE"
+ SUPERUSER_ROLE = nil
+ end
+
+ def self.included(base)
+ base.extend ClassMethods
+ base.send(:include, InstanceMethods)
+ end
+
+ class AuthorizedContextError < StandardError; end
+ class NoRulesFoundForContext < AuthorizedContextError; end
+ class NoRulesFoundForController < AuthorizedContextError; end
+ class AccessDenied < AuthorizedContextError; end
+ class NoRoleForPermissions < AuthorizedContextError; end
+ class BadTypeProvidedForPermission < AuthorizedContextError; end
+ class BadTypeProvidedForRole < AuthorizedContextError; end
+ class AccessDenied < AuthorizedContextError; end
+ class LoginRequired < AuthorizedContextError; end
+
+ class Permissions
+
+ def initialize(controller)
+ @controller = controller
+ @permissions = {}
+ end
+
+ def [](permission)
+ @permissions[permission]
+ end
+
+ def []=(permission)
+ @permissions[permission]
+ end
+
+ def grant(who)
+ raise BadTypeProvidedForRole unless who.is_a?(String)
+ @actual_role = who
+ end
+
+ def can(*perms)
+ raise NoRoleForPermissions unless @actual_role
+
+ perms.each do |p|
+ case p
+ when String
+ @permissions[p] = @actual_role
+ when Hash
+ p.each do |context, actions|
+ @permissions[context] = @actual_role
+ actions = [actions] unless actions.is_a? Array
+ actions.each do |action|
+ @controller.class_eval do
+ before_filter :only =>actions do |controller|
+ debugger
+ controller.authorized_context(context)
+ end
+ end
+ end
+ end
+ else
+ raise BadTypeProvidedForPermission
+ end
+ end
+ end
+
+ end
+
+ module ClassMethods
+
+ def grant(*args, &block)
+ if block_given?
+ @@permission_rules = Permissions.new(self)
+ @@permission_rules.instance_eval(&block)
+ end
+ unless args.empty?
+ permit(*args)
+ end
+ end
+
+ def permission_rules
+ @@permission_rules
+ end
+
+ end
+
+ module InstanceMethods
+
+ def authorized_context(name, *args, &blk)
+ @options = { :permission_denied_message => PERMISSION_DENIED_MESSAGE, :login_required_message => LOGIN_REQUIRED_MESSAGE }
+ @options.merge!( args.last.is_a?( Hash ) ? args.last : {} )
+ permit_context(name, @options, &blk)
+ end
+
+ def authorized_to?(name, *args)
+ return true if AUTHORIZATION_SUPERUSER_ROLE && current_user.has_role?(AUTHORIZATION_SUPERUSER_ROLE)
+ check_permission_rules_for_controller
+ @options = { :allow_guests => false, :redirect => true }
+ @options.merge!( args.last.is_a?( Hash ) ? args.last : {} )
+ check_permission_rules_for_context(name)
+ authorization_expression = permission_rules[name]
+ permit? authorization_expression, *args
+ end
+
+ def permission_rules
+ if defined? controller
+ controller.permission_rules
+ else
+ self.class.permission_rules
+ end
+ end
+
+ # Added on_access_denied and on_login_required event handlers calls
+ def permit( authorization_expression, *args )
+ @options = { :allow_guests => false, :redirect => true }
+ @options.merge!( args.last.is_a?( Hash ) ? args.last : {} )
+ if has_permission?( authorization_expression) || (SUPERUSER_ROLE && @current_user.has_role?(SUPERUSER_ROLE))
+ yield if block_given?
+ elsif @current_user && @current_user != :false && self.respond_to?(:on_access_denied)
+ self.send(:on_access_denied, @actual_authorized_context) and return false
+ elsif (!@current_user || @current_user == :false) && self.respond_to?(:on_login_required)
+ self.send(:on_login_required, @actual_authorized_context) and return false
+ elsif @options[:redirect]
+ handle_redirection
+ end
+ end
+
+ private
+
+ def permit_context(name, *args, &blk)
+ check_permission_rules_for_controller
+ check_permission_rules_for_context(name)
+ @actual_authorized_context = name
+ authorization_expression = permission_rules[name]
+ permit(authorization_expression, *args, &blk)
+ end
+
+ def check_permission_rules_for_controller
+ unless defined? permission_rules
+ then raise( NoRulesFoundForController, "No permission rules found for controller \"#{controller_name}\". Set the permissions first!") end
+ end
+
+ def check_permission_rules_for_context(context_name)
+ if permission_rules[context_name].blank?
+ then raise( NoRulesFoundForContext, "\"#{context_name}\" -- No permission rules found for this context. Please, check the name of the context or set the permissions rules.") end
+ end
+
+ end
+ end
+end
71 lib/computed_roles.rb
@@ -0,0 +1,71 @@
+module Authorization
+ module Base
+ module ComputedRoles
+
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ class AuthorizationExpressionInvalid < StandardError; end;
+
+ module ClassMethods
+
+ def acts_as_authorized_user(roles_relationship_opts = {})
+ super
+ class_inheritable_hash :computed_roles, :computed_relationships
+ self.computed_relationships = {}
+ self.computed_roles = {}
+ include Authorization::Base::ComputedRoles::InstanceMethods
+ end
+
+ def has_role(str, opts={})
+ if str =~ /[^A-Za-z0-9_:'\(\)\s]/
+ raise AuthorizationExpressionInvalid, "Invalid authorization expression (#{str})"
+ return false
+ end
+ role_regex = '\s*(\'\s*(.+?)\s*\'|(\w+))\s+'
+ just_role_regex = /\s*(\'\s*(.+?)\s*\'|(\w+))\s*/
+ model_regex = '\s+(:*\w+)'
+ relationship_regex = Regexp.new(role_regex + '(' + VALID_PREPOSITIONS.join('|') + ')' + model_regex)
+ (str =~ relationship_regex) || (str =~ just_role_regex)
+ role = $1 || $2
+ model = $5
+ cond = opts[:if]
+ if model
+ computed_relationships[model] ||= {}
+ computed_relationships[model][role] = cond
+ else
+ computed_roles[role] = cond
+ end
+ end
+
+ end
+
+ module InstanceMethods
+
+ def has_role?(role, auth_object=nil)
+ rels = computed_relationships
+ rols = computed_roles
+ model = auth_object.class.to_s.underscore if auth_object
+ if model && rels.has_key?(model) && rels[model].has_key?(role)
+ rels[model][role].call(self, auth_object)
+ elsif rols.has_key?(role)
+ rols[role].call(self)
+ else
+ super
+ end
+ end
+
+ def computed_relationships
+ self.class.computed_relationships
+ end
+
+ def computed_roles
+ self.class.computed_roles
+ end
+
+ end
+
+ end
+ end
+end
43 lib/publishare/exceptions.rb
@@ -0,0 +1,43 @@
+module Authorization #:nodoc:
+
+ # Base error class for Authorization module
+ class AuthorizationError < StandardError
+ end
+
+ # Raised when the authorization expression is invalid (cannot be parsed)
+ class AuthorizationExpressionInvalid < AuthorizationError
+ end
+
+ # Raised when we can't find the current user
+ class CannotObtainUserObject < AuthorizationError
+ end
+
+ # Raised when an authorization expression contains a model class that doesn't exist
+ class CannotObtainModelClass < AuthorizationError
+ end
+
+ # Raised when an authorization expression contains a model reference that doesn't exist
+ class CannotObtainModelObject < AuthorizationError
+ end
+
+ # Raised when the obtained user object doesn't implement #id
+ class UserDoesntImplementID < AuthorizationError
+ end
+
+ # Raised when the obtained user object doesn't implement #has_role?
+ class UserDoesntImplementRoles < AuthorizationError
+ end
+
+ # Raised when the obtained model doesn't implement #accepts_role?
+ class ModelDoesntImplementRoles < AuthorizationError
+ end
+
+ class CannotSetRoleWhenHardwired < AuthorizationError
+ end
+
+ class CannotSetObjectRoleWhenSimpleRoleTable < AuthorizationError
+ end
+
+ class CannotGetAuthorizables < AuthorizationError
+ end
+end
82 lib/publishare/hardwired_roles.rb
@@ -0,0 +1,82 @@
+require File.dirname(__FILE__) + '/exceptions'
+
+# In order to use this mixin, you'll need to define roles by overriding the
+# following functions:
+#
+# User#has_role?(role)
+# Return true or false depending on the roles (strings) passed in.
+#
+# Model#accepts_role?(role, user)
+# Return true or false depending on the roles (strings) this particular user has for
+# this particular model object.
+#
+# See http://www.writertopia.com/developers/authorization
+
+module Authorization
+ module HardwiredRoles
+
+ module UserExtensions
+ def self.included( recipient )
+ recipient.extend( ClassMethods )
+ end
+
+ module ClassMethods
+ def acts_as_authorized_user
+ include Authorization::HardwiredRoles::UserExtensions::InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ # If roles aren't explicitly defined in user class then return false
+ def has_role?( role, authorizable_object = nil )
+ false
+ end
+
+ def has_role( role, authorizable_object = nil )
+ raise( CannotSetRoleWhenHardwired,
+ "Hardwired mixin: Cannot set user to role #{role}. Don't use #has_role, use code in models."
+ )
+ end
+
+ def has_no_role( role, authorizable_object = nil )
+ raise( CannotSetRoleWhenHardwired,
+ "Hardwired mixin: Cannot remove user role #{role}. Don't use #has_no_role, use code in models."
+ )
+ end
+ end
+ end
+
+ module ModelExtensions
+ def self.included( recipient )
+ recipient.extend( ClassMethods )
+ end
+
+ module ClassMethods
+ def acts_as_authorizable
+ include Authorization::HardwiredRoles::ModelExtensions::InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ def accepts_role?( role, user )
+ return false
+ end
+
+ def accepts_role( role, user )
+ raise( CannotSetRoleWhenHardwired,
+ "Hardwired mixin: Cannot set user to role #{role}. Don't use #accepts_role, use code in models."
+ )
+ end
+
+ def accepts_no_role( role, user )
+ raise( CannotSetRoleWhenHardwired,
+ "Hardwired mixin: Cannot set user to role #{role}. Don't use #accepts_no_role, use code in models."
+ )
+ end
+ end
+ end
+
+ end
+
+end
+
135 lib/publishare/identity.rb
@@ -0,0 +1,135 @@
+require File.dirname(__FILE__) + '/exceptions'
+
+# Provides the appearance of dynamically generated methods on the roles database.
+#
+# Examples:
+# user.is_member? --> Returns true if user has any role of "member"
+# user.is_member_of? this_workshop --> Returns true/false. Must have authorizable object after query.
+# user.is_eligible_for [this_award] --> Gives user the role "eligible" for "this_award"
+# user.is_moderator --> Gives user the general role "moderator" (not tied to any class or object)
+# user.is_candidate_of_what --> Returns array of objects for which this user is a "candidate" (any type)
+# user.is_candidate_of_what(Party) --> Returns array of objects for which this user is a "candidate" (only 'Party' type)
+#
+# model.has_members --> Returns array of users which have role "member" on that model
+# model.has_members? --> Returns true/false
+#
+module Authorization
+ module Identity
+
+ module UserExtensions
+ module InstanceMethods
+
+ def method_missing( method_sym, *args )
+ method_name = method_sym.to_s
+ authorizable_object = args.empty? ? nil : args[0]
+
+ base_regex = "is_(\\w+)"
+ fancy_regex = base_regex + "_(#{Authorization::Base::VALID_PREPOSITIONS_PATTERN})"
+ is_either_regex = '^((' + fancy_regex + ')|(' + base_regex + '))'
+ base_not_regex = "is_no[t]?_(\\w+)"
+ fancy_not_regex = base_not_regex + "_(#{Authorization::Base::VALID_PREPOSITIONS_PATTERN})"
+ is_not_either_regex = '^((' + fancy_not_regex + ')|(' + base_not_regex + '))'
+
+ if method_name =~ Regexp.new(is_either_regex + '_what$')
+ role_name = $3 || $6
+ has_role_for_objects(role_name, authorizable_object)
+ elsif method_name =~ Regexp.new(is_not_either_regex + '\?$')
+ role_name = $3 || $6
+ not is_role?( role_name, authorizable_object )
+ elsif method_name =~ Regexp.new(is_either_regex + '\?$')
+ role_name = $3 || $6
+ is_role?( role_name, authorizable_object )
+ elsif method_name =~ Regexp.new(is_not_either_regex + '$')
+ role_name = $3 || $6
+ is_no_role( role_name, authorizable_object )
+ elsif method_name =~ Regexp.new(is_either_regex + '$')
+ role_name = $3 || $6
+ is_role( role_name, authorizable_object )
+ else
+ super
+ end
+ end
+
+ private
+
+ def is_role?( role_name, authorizable_object )
+ if authorizable_object.nil?
+ return self.has_role?(role_name)
+ elsif authorizable_object.respond_to?(:accepts_role?)
+ return self.has_role?(role_name, authorizable_object)
+ end
+ false
+ end
+
+ def is_no_role( role_name, authorizable_object = nil )
+ if authorizable_object.nil?
+ self.has_no_role role_name
+ else
+ self.has_no_role role_name, authorizable_object
+ end
+ end
+
+ def is_role( role_name, authorizable_object = nil )
+ if authorizable_object.nil?
+ self.has_role role_name
+ else
+ self.has_role role_name, authorizable_object
+ end
+ end
+
+ def has_role_for_objects(role_name, type)
+ if type.nil?
+ roles = self.roles.find_all_by_name( role_name )
+ else
+ roles = self.roles.find_all_by_authorizable_type_and_name( type.name, role_name )
+ end
+ roles.collect do |role|
+ if role.authorizable_id.nil?
+ role.authorizable_type.nil? ?
+ nil : Module.const_get( role.authorizable_type ) # Returns class
+ else
+ role.authorizable
+ end
+ end
+ end
+ end
+ end
+
+ module ModelExtensions
+ module InstanceMethods
+
+ def method_missing( method_sym, *args )
+ method_name = method_sym.to_s
+ if method_name =~ /^has_(\w+)\?$/
+ roles = roles_array_from($1)
+ user_count_of(roles) > 0
+ elsif method_name =~ /^has_(\w+)_count$/
+ roles = roles_array_from($1)
+ user_count_of(roles)
+ elsif method_name =~ /^has_(\w+)$/
+ roles = roles_array_from($1)
+ users = self.accepted_roles.find_all_by_name(roles, :include => :users).collect { |role| role.users }
+ users.flatten.compact.uniq if users
+ else
+ super
+ end
+ end
+
+ private
+
+ def roles_array_from(string)
+ roles = string.split('_or_').collect { |role| role.singularize }
+ roles.flatten.compact
+ end
+
+ def user_count_of(roles)
+ count = 0
+ self.accepted_roles.find_all_by_name(roles).each { |role| count += role.users.count }
+ count
+ end
+
+ end
+ end
+
+ end
+end
203 lib/publishare/object_roles_table.rb
@@ -0,0 +1,203 @@
+require File.dirname(__FILE__) + '/exceptions'
+require File.dirname(__FILE__) + '/identity'
+
+module Authorization
+ module ObjectRolesTable
+
+ module UserExtensions
+ def self.included( recipient )
+ recipient.extend( ClassMethods )
+ end
+
+ module ClassMethods
+ def acts_as_authorized_user(roles_relationship_opts = {})
+ has_many :roles_users, roles_relationship_opts.merge(:dependent => :delete_all)
+ has_many :roles, :through => :roles_users
+ include Authorization::ObjectRolesTable::UserExtensions::InstanceMethods
+ include Authorization::Identity::UserExtensions::InstanceMethods # Provides all kinds of dynamic sugar via method_missing
+ end
+ end
+
+ module InstanceMethods
+ # If roles aren't explicitly defined in user class then check roles table
+ def has_role?( role_name, authorizable_obj = nil )
+ if authorizable_obj.nil?
+ self.roles.find_by_name( role_name ) || self.roles.member?(get_role( role_name, authorizable_obj )) ? true : false # If we ask a general role question, return true if any role is defined.
+ else
+ role = get_role( role_name, authorizable_obj )
+ role ? self.roles.exists?( role.id ) : false
+ end
+ end
+
+ def has_role( role_name, authorizable_obj = nil )
+ role = get_role( role_name, authorizable_obj )
+ if role.nil?
+ if authorizable_obj.is_a? Class
+ role = Role.create( :name => role_name, :authorizable_type => authorizable_obj.to_s )
+ elsif authorizable_obj
+ role = Role.create( :name => role_name, :authorizable => authorizable_obj )
+ else
+ role = Role.create( :name => role_name )
+ end
+ end
+ self.roles << role if role and not self.roles.exists?( role.id )
+ end
+
+ def has_no_role( role_name, authorizable_obj = nil )
+ role = get_role( role_name, authorizable_obj )
+ self.roles.delete( role ) if role
+ delete_role_if_empty( role )
+ end
+
+ def has_roles_for?( authorizable_obj )
+ if authorizable_obj.is_a? Class
+ !self.roles.detect { |role| role.authorizable_type == authorizable_obj.to_s }.nil?
+ elsif authorizable_obj
+ !self.roles.detect { |role| role.authorizable_type == authorizable_obj.class.base_class.to_s && role.authorizable == authorizable_obj }.nil?
+ else
+ !self.roles.detect { |role| role.authorizable.nil? }.nil?
+ end
+ end
+ alias :has_role_for? :has_roles_for?
+
+ def roles_for( authorizable_obj )
+ if authorizable_obj.is_a? Class
+ self.roles.find(:all, :conditions => { :authorizable_type => authorizable_obj.to_s})
+ elsif authorizable_obj
+ self.roles.find(:all, :conditions => {
+ :authorizable_type => authorizable_obj.class.base_class.to_s,
+ :authorizable_id => authorizable_obj.id })
+ else
+ self.roles.select { |role| role.authorizable.nil? }
+ end
+ end
+
+ def has_no_roles_for(authorizable_obj = nil)
+ old_roles = roles_for(authorizable_obj).dup
+ self.roles.delete(old_roles)
+ old_roles.each { |role| delete_role_if_empty( role ) }
+ end
+
+ def has_no_roles
+ old_roles = self.roles.dup
+ self.roles.clear
+ old_roles.each { |role| delete_role_if_empty( role ) }
+ end
+
+ def authorizables_for( authorizable_class )
+ unless authorizable_class.is_a? Class
+ raise CannotGetAuthorizables, "Invalid argument: '#{authorizable_class}'. You must provide a class here."
+ end
+ begin
+ authorizable_class.find(
+ self.roles.find_all_by_authorizable_type(authorizable_class.base_class.to_s).map(&:authorizable_id).uniq
+ )
+ rescue ActiveRecord::RecordNotFound
+ []
+ end
+ end
+
+ private
+
+ def get_role( role_name, authorizable_obj )
+ if authorizable_obj.is_a? Class
+ Role.find( :first,
+ :conditions => [ 'name = ? and authorizable_type = ? and authorizable_id IS NULL', role_name, authorizable_obj.to_s ] )
+ elsif authorizable_obj
+ Role.find( :first,
+ :conditions => [ 'name = ? and authorizable_type = ? and authorizable_id = ?',
+ role_name, authorizable_obj.class.base_class.to_s, authorizable_obj.id ] )
+ else
+ Role.find( :first,
+ :conditions => [ 'name = ? and authorizable_type IS NULL and authorizable_id IS NULL', role_name ] )
+ end
+ end
+
+ def delete_role_if_empty( role )
+ role.destroy if role && role.users.count == 0
+ end
+
+ end
+ end
+
+ module ModelExtensions
+ def self.included( recipient )
+ recipient.extend( ClassMethods )
+ end
+
+ module ClassMethods
+ def acts_as_authorizable
+ has_many :accepted_roles, :as => :authorizable, :class_name => 'Role'
+
+ has_many :users, :finder_sql => 'SELECT DISTINCT users.* FROM users INNER JOIN roles_users ON user_id = users.id INNER JOIN roles ON roles.id = role_id WHERE authorizable_type = \'#{self.class.base_class.to_s}\' AND authorizable_id = #{id}', :counter_sql => 'SELECT COUNT(DISTINCT users.id) FROM users INNER JOIN roles_users ON user_id = users.id INNER JOIN roles ON roles.id = role_id WHERE authorizable_type = \'#{self.class.base_class.to_s}\' AND authorizable_id = #{id}', :readonly => true
+
+ before_destroy :remove_user_roles
+
+ def accepts_role?( role_name, user )
+ user.has_role? role_name, self
+ end
+
+ def accepts_role( role_name, user )
+ user.has_role role_name, self
+ end
+
+ def accepts_no_role( role_name, user )
+ user.has_no_role role_name, self
+ end
+
+ def accepts_roles_by?( user )
+ user.has_roles_for? self
+ end
+ alias :accepts_role_by? :accepts_roles_by?
+
+ def accepted_roles_by( user )
+ user.roles_for self
+ end
+
+ def authorizables_by( user )
+ user.authorizables_for self
+ end
+
+ include Authorization::ObjectRolesTable::ModelExtensions::InstanceMethods
+ include Authorization::Identity::ModelExtensions::InstanceMethods # Provides all kinds of dynamic sugar via method_missing
+ end
+ end
+
+ module InstanceMethods
+ # If roles aren't overriden in model then check roles table
+ def accepts_role?( role_name, user )
+ user.has_role? role_name, self
+ end
+
+ def accepts_role( role_name, user )
+ user.has_role role_name, self
+ end
+
+ def accepts_no_role( role_name, user )
+ user.has_no_role role_name, self
+ end
+
+ def accepts_roles_by?( user )
+ user.has_roles_for? self
+ end
+ alias :accepts_role_by? :accepts_roles_by?
+
+ def accepted_roles_by( user )
+ user.roles_for self
+ end
+
+ private
+
+ def remove_user_roles
+ self.accepted_roles.each do |role|
+ role.roles_users.delete_all
+ role.destroy
+ end
+ end
+
+ end
+ end
+
+ end
+end
+
210 lib/publishare/parser.rb
@@ -0,0 +1,210 @@
+module Authorization
+ module Base
+
+ VALID_PREPOSITIONS = ['of', 'for', 'in', 'on', 'to', 'at', 'by']
+ BOOLEAN_OPS = ['not', 'or', 'and']
+ VALID_PREPOSITIONS_PATTERN = VALID_PREPOSITIONS.join('|')
+
+ module EvalParser
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
+ #
+ # The authorization expression is defined by the following grammar:
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
+ # <term> ::= <role> | <role> <preposition> <model>
+ # <preposition> ::= of | for | in | on | to | at | by
+ # <model> ::= /:*\w+/
+ # <role> ::= /\w+/ | /'.*'/
+ #
+ # Instead of doing recursive descent parsing (not so fun when we support nested parentheses, etc),
+ # we let Ruby do the work for us by inserting the appropriate permission calls and using eval.
+ # This would not be a good idea if you were getting authorization expressions from the outside,
+ # so in that case (e.g. somehow letting users literally type in permission expressions) you'd
+ # be better off using the recursive descent parser in Module RecursiveDescentParser.
+ #
+ # We search for parts of our authorization evaluation that match <role> or <role> <preposition> <model>
+ # and we ignore anything terminal in our grammar.
+ #
+ # 1) Replace all <role> <preposition> <model> matches.
+ # 2) Replace all <role> matches that aren't one of our other terminals ('not', 'or', 'and', or preposition)
+ # 3) Eval
+
+ def parse_authorization_expression( str )
+ if str =~ /[^A-Za-z0-9_:'\(\)\s]/
+ raise AuthorizationExpressionInvalid, "Invalid authorization expression (#{str})"
+ return false
+ end
+ @replacements = []
+ expr = replace_temporarily_role_of_model( str )
+ expr = replace_role( expr )
+ expr = replace_role_of_model( expr )
+ begin
+ instance_eval( expr )
+ rescue Exception => error
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str}): #{error.message}"
+ end
+ end
+
+ def replace_temporarily_role_of_model( str )
+ role_regex = '\s*(\'\s*(.+?)\s*\'|(\w+))\s+'
+ model_regex = '\s+(:*\w+)'
+ parse_regex = Regexp.new(role_regex + '(' + VALID_PREPOSITIONS.join('|') + ')' + model_regex)
+ str.gsub(parse_regex) do |match|
+ @replacements.push " process_role_of_model('#{$2 || $3}', '#{$5}') "
+ " <#{@replacements.length-1}> "
+ end
+ end
+
+ def replace_role( str )
+ role_regex = '\s*(\'\s*(.+?)\s*\'|([A-Za-z]\w*))\s*'
+ parse_regex = Regexp.new(role_regex)
+ str.gsub(parse_regex) do |match|
+ if BOOLEAN_OPS.include?($3)
+ " #{match} "
+ else
+ " process_role('#{$2 || $3}') "
+ end
+ end
+ end
+
+ def replace_role_of_model( str )
+ str.gsub(/<(\d+)>/) do |match|
+ @replacements[$1.to_i]
+ end
+ end
+
+ def process_role_of_model( role_name, model_name )
+ model = get_model( model_name )
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model.respond_to? :accepts_role?
+ model.send( :accepts_role?, role_name, @current_user )
+ end
+
+ def process_role( role_name )
+ return false if @current_user.nil? || @current_user == :false
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
+ @current_user.has_role?( role_name )
+ end
+
+ end
+
+ # Parses and evaluates an authorization expression and returns <tt>true</tt> or <tt>false</tt>.
+ # This recursive descent parses uses two instance variables:
+ # @stack --> a stack with the top holding the boolean expression resulting from the parsing
+ #
+ # The authorization expression is defined by the following grammar:
+ # <expr> ::= (<expr>) | not <expr> | <term> or <expr> | <term> and <expr> | <term>
+ # <term> ::= <role> | <role> <preposition> <model>
+ # <preposition> ::= of | for | in | on | to | at | by
+ # <model> ::= /:*\w+/
+ # <role> ::= /\w+/ | /'.*'/
+ #
+ # There are really two values we must track:
+ # (1) whether the expression is valid according to the grammar
+ # (2) the evaluated results --> true/false on the permission queries
+ # The first is embedded in the control logic because we want short-circuiting. If an expression
+ # has been parsed and the permission is false, we don't want to try different ways of parsing.
+ # Note that this implementation of a recursive descent parser is meant to be simple
+ # and doesn't allow arbitrary nesting of parentheses. It supports up to 5 levels of nesting.
+ # It also won't handle some types of expressions (A or B) and C, which has to be rewritten as
+ # C and (A or B) so the parenthetical expressions are in the tail.
+ module RecursiveDescentParser
+
+ OPT_PARENTHESES_PATTERN = '(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()]|\(([^()])*\))*\))*\))*\))*\))*)'
+ PARENTHESES_PATTERN = '\(' + OPT_PARENTHESES_PATTERN + '\)'
+ NOT_PATTERN = '^\s*not\s+' + OPT_PARENTHESES_PATTERN + '$'
+ AND_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+and\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
+ OR_PATTERN = '^\s*' + OPT_PARENTHESES_PATTERN + '\s+or\s+' + OPT_PARENTHESES_PATTERN + '\s*$'
+ ROLE_PATTERN = '(\'\s*(.+)\s*\'|(\w+))'
+ MODEL_PATTERN = '(:*\w+)'
+
+ PARENTHESES_REGEX = Regexp.new('^\s*' + PARENTHESES_PATTERN + '\s*$')
+ NOT_REGEX = Regexp.new(NOT_PATTERN)
+ AND_REGEX = Regexp.new(AND_PATTERN)
+ OR_REGEX = Regexp.new(OR_PATTERN)
+ ROLE_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s*$')
+ ROLE_OF_MODEL_REGEX = Regexp.new('^\s*' + ROLE_PATTERN + '\s+(' + VALID_PREPOSITIONS_PATTERN + ')\s+' + MODEL_PATTERN + '\s*$')
+
+ def parse_authorization_expression( str )
+ @stack = []
+ raise AuthorizationExpressionInvalid, "Cannot parse authorization (#{str})" if not parse_expr( str )
+ return @stack.pop
+ end
+
+ def parse_expr( str )
+ parse_parenthesis( str ) or
+ parse_not( str ) or
+ parse_or( str ) or
+ parse_and( str ) or
+ parse_term( str )
+ end
+
+ def parse_not( str )
+ if str =~ NOT_REGEX
+ can_parse = parse_expr( $1 )
+ @stack.push( !@stack.pop ) if can_parse
+ end
+ false
+ end
+
+ def parse_or( str )
+ if str =~ OR_REGEX
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
+ @stack.push( @stack.pop | @stack.pop ) if can_parse
+ return can_parse
+ end
+ false
+ end
+
+ def parse_and( str )
+ if str =~ AND_REGEX
+ can_parse = parse_expr( $1 ) and parse_expr( $8 )
+ @stack.push(@stack.pop & @stack.pop) if can_parse
+ return can_parse
+ end
+ false
+ end
+
+ # Descend down parenthesis (allow up to 5 levels of nesting)
+ def parse_parenthesis( str )
+ str =~ PARENTHESES_REGEX ? parse_expr( $1 ) : false
+ end
+
+ def parse_term( str )
+ parse_role_of_model( str ) or
+ parse_role( str )
+ end
+
+ # Parse <role> of <model>
+ def parse_role_of_model( str )
+ if str =~ ROLE_OF_MODEL_REGEX
+ role_name = $2 || $3
+ model_name = $5
+ model_obj = get_model( model_name )
+ raise( ModelDoesntImplementRoles, "Model (#{model_name}) doesn't implement #accepts_role?" ) if not model_obj.respond_to? :accepts_role?
+
+ has_permission = model_obj.send( :accepts_role?, role_name, @current_user )
+ @stack.push( has_permission )
+ true
+ else
+ false
+ end
+ end
+
+ # Parse <role> of the User-like object
+ def parse_role( str )
+ if str =~ ROLE_REGEX
+ role_name = $1
+ if @current_user.nil? || @current_user == :false
+ @stack.push(false)
+ else
+ raise( UserDoesntImplementRoles, "User doesn't implement #has_role?" ) if not @current_user.respond_to? :has_role?
+ @stack.push( @current_user.has_role?(role_name) )
+ end
+ true
+ else
+ false
+ end
+ end
+
+ end
+ end
+end
4 tasks/authorization_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :authorization do
+# # Task goes here
+# end
3  test/README.txt
@@ -0,0 +1,3 @@
+The tests for this plugin are maintained in a sample
+Ruby on Rails application. Please see the file
+README_developers.txt for additional information.
Please sign in to comment.
Something went wrong with that request. Please try again.