with_role #16

Merged
merged 17 commits into from Dec 8, 2011
+248 −75
Split
View
@@ -1,7 +1,8 @@
PATH
remote: .
specs:
- easy_roles (1.2.0)
+ easy_roles (2.0.0.beta2)
+ activesupport
GEM
remote: http://rubygems.org/
View
@@ -26,7 +26,7 @@ Then generate the migration:
Or add a "roles" column to your users model, and set the default value to "--- []". Please note you can call this column anything you like, I like to use the name "roles".
- t.string :roles, :default => "--- []"
+ t.string :roles, default: "--- []"
Then you need to add "easy_roles :column_name" to your model:
@@ -46,20 +46,20 @@ Then generate the migration:
Or add a "roles_mask" column to your users model of type 'integer', and set the default value to 0. Please note you can call this column anything you like, I like to use the name "roles_mask":
- t.integer :roles_mask, :default => 0
+ t.integer :roles_mask, default: 0
-Add "easy_roles :column_name, :method => :bitmask" to your model.
+Add "easy_roles :column_name, method: :bitmask" to your model.
class User < ActiveRecord::Base
- easy_roles :roles_mask, :method => :bitmask
+ easy_roles :roles_mask, method: :bitmask
end
And lastly you need to add a constant variable which stores an array of the different roles for your system. The name of the constant must be the name of your column in full caps.
==== WARNING: Bitmask storage relies that you DO NOT change the order of your array of roles, if you need to add a new role, just append it to the end of the array.
class User < ActiveRecord::Base
- easy_roles :roles_mask, :method => :bitmask
+ easy_roles :roles_mask, method: :bitmask
# Constant variable storing roles in the system
ROLES_MASK = %w[admin moderator user]
@@ -133,9 +133,23 @@ Then in your AdminsController or any controller that you only want admins to vie
end
class MarksController < ApplicationController
- before_filter :admin_required, :only => :create, :update
+ before_filter :admin_required, only: %w(create update)
end
+== Scopes
+
+By default, easy_roles adds the `with_role` scope to your models.
+
+ @admins = User.with_role('admin')
+
+If you're using the bitmask method, an ArgumentError will be thrown if an undeclared scope is queried. Since an `ActiveRecord::Relation` is returned, the query is chainable:
+
+ BitmaskUser.with_role('admin').where(active: true).to_sql
+ # => SELECT "bitmask_users".* FROM "bitmask_users" WHERE "bitmask_users"."roles_mask" IN (1, 3, 5, 7) AND "bitmask_users"."active" = 't'
+
+ SerializeUser.with_role('admin').where(active: true).to_sql
+ # => SELECT "serialize_users".* FROM "serialize_users" WHERE "serialize_users"."active" = 't' AND (serialize_users.roles LIKE "%!admin!%")
+
Follow me on twitter: http://twitter.com/ryan_za
Email: ryan *at* platform45.com
@@ -161,4 +175,4 @@ 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.
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
@@ -16,17 +16,13 @@ module EasyRoles
module ClassMethods
def easy_roles(name, options = {})
- options[:method] ||= :serialize
-
begin
raise NameError unless ALLOWED_METHODS.include? options[:method]
-
- "EasyRoles::#{options[:method].to_s.camelize}".constantize.new(self, name, options)
rescue NameError
puts "[Easy Roles] Storage method does not exist reverting to Serialize"
-
- EasyRoles::Serialize.new(self, name, options)
+ options[:method] = :serialize
end
+ "EasyRoles::#{options[:method].to_s.camelize}".constantize.new(self, name, options)
end
end
@@ -4,20 +4,23 @@ module ActiveRecord
module Generators
class EasyRolesGenerator < ActiveRecord::Generators::Base
- argument :role_col, :type => :string, :required => false, :default => "roles", :banner => "role column"
+ argument :role_col, type: :string, required: false, default: 'roles', banner: 'role column'
- class_option :use_bitmask_method, :type => :boolean, :required => false, :default => false,
- :desc => "Setup migration for Bitmask method"
+ class_option :use_bitmask_method, type: :boolean, required: false, default: false,
+ desc: 'Setup migration for Bitmask method'
- desc "Internal use by easy_roles generator - use that instead"
+ class_option :add_index, type: :boolean, required: false, default: false,
+ desc: 'Add an index to the relevant column'
- source_root File.expand_path("../templates", __FILE__)
+ desc 'Internal use by easy_roles generator - use that instead'
+
+ source_root File.expand_path('../templates', __FILE__)
def copy_easy_roles_migration
if options.use_bitmask_method
- migration_template "migration_bitmask.rb", "db/migrate/add_bitmask_roles_to_#{table_name}"
+ migration_template 'migration_bitmask.rb', "db/migrate/add_bitmask_roles_to_#{table_name}"
else
- migration_template "migration_non_bitmask.rb", "db/migrate/add_easy_roles_to_#{table_name}"
+ migration_template 'migration_non_bitmask.rb', "db/migrate/add_easy_roles_to_#{table_name}"
end
end
@@ -1,9 +1,10 @@
class AddBitmaskRolesTo<%= table_name.camelize %> < ActiveRecord::Migration
- def self.up
- add_column :<%= table_name %>, :<%= self.role_col %>, :integer, :default => 0
- end
-
- def self.down
- remove_column :<%= table_name.to_sym %>, :<%= self.role_col %>
+ def change
+ change_table :<%= table_name %> do |t|
+ t.integer :<%= self.role_col %>, default: 0
+ <%- if options.add_index -%>
+ t.index :<%= self.role_col %>
+ <%- end -%>
+ end
end
end
@@ -1,9 +1,10 @@
class AddEasyRolesTo<%= table_name.camelize %> < ActiveRecord::Migration
- def self.up
- add_column :<%= table_name %>, :<%= self.role_col %>, :string, :default => "--- []"
+ def change
+ change_table :<%= table_name %> do |t|
+ t.string :<%= self.role_col %>, default: '--- []'
+ <%- if options.add_index -%>
+ t.index :<%= self.role_col %>
+ <%- end -%>
+ end
end
-
- def self.down
- remove_column :<%= table_name.to_sym %>, :<%= self.role_col %>
- end
-end
+end
@@ -1,21 +1,21 @@
module EasyRoles
module Generators
class EasyRolesGenerator < Rails::Generators::NamedBase
- namespace "easy_roles"
+ namespace 'easy_roles'
- argument :role_col, :type => :string, :required => false, :default => "roles", :banner => "role column"
+ argument :role_col, type: :string, required: false, default: 'roles', banner: 'role column'
- class_option :use_bitmask_method, :type => :boolean, :required => false, :default => false,
- :desc => "Setup migration for Bitmask method"
+ class_option :use_bitmask_method, type: :boolean, required: false, default: false,
+ desc: 'Setup migration for Bitmask method'
- desc "Create ActiveRecord migration for easy_roles on NAME model using [ROLE] column -- defaults to 'roles'"
+ desc 'Create ActiveRecord migration for easy_roles on NAME model using [ROLE] column -- defaults to \'roles\''
source_root File.expand_path('../../templates', __FILE__)
hook_for :orm
def show_readme
- readme "README" if behavior == :invoke
+ readme 'README' if behavior == :invoke
end
end
@@ -1,6 +1,7 @@
module EasyRoles
class Bitmask
def initialize(base, column_name, options)
+
base.send :define_method, :_roles= do |roles|
states = base.const_get(column_name.upcase.to_sym)
@@ -9,8 +10,9 @@ def initialize(base, column_name, options)
base.send :define_method, :_roles do
states = base.const_get(column_name.upcase.to_sym)
+ masked_integer = self[column_name.to_sym] || 0
- states.reject { |r| ((self[column_name.to_sym] || 0) & 2**states.index(r)).zero? }
+ states.reject.with_index { |r,i| masked_integer[i].zero? }
end
base.send :define_method, :has_role? do |role|
@@ -50,6 +52,18 @@ def initialize(base, column_name, options)
self.save!
end
+
+ base.class_eval do
+ scope :with_role, proc { |role|
+ states = base.const_get(column_name.upcase.to_sym)
+ raise ArgumentError unless states.include? role
+ role_bit_index = states.index(role)
+ valid_mask_integers = (0..2**states.count-1).select {|i| i[role_bit_index] == 1 }
+ where(column_name => valid_mask_integers)
+ }
+ end
+
+
end
end
end
@@ -1,25 +1,32 @@
module EasyRoles
class Serialize
+
def initialize(base, column_name, options)
base.serialize column_name.to_sym, Array
ActiveSupport::Deprecation.silence do
- base.before_validation(:make_default_roles, :on => :create)
+ base.before_validation(:make_default_roles, on: :create)
end
-
+
base.send :define_method, :has_role? do |role|
self[column_name.to_sym].include?(role)
end
base.send :define_method, :add_role do |role|
clear_roles if self[column_name.to_sym].blank?
+ marker = base::ROLES_MARKER
+ return false if (!marker.empty? && role.include?(marker))
+
has_role?(role) ? false : self[column_name.to_sym] << role
end
base.send :define_method, :add_role! do |role|
- add_role(role)
- self.save!
+ if add_role(role)
+ self.save!
+ else
+ return false
+ end
end
base.send :define_method, :remove_role do |role|
@@ -40,6 +47,45 @@ def initialize(base, column_name, options)
end
base.send :private, :make_default_roles
+
+ # Scopes:
+ # ---------
+ # For security, wrapping markers must be included in the LIKE search, otherwise a user with
+ # role 'administrator' would erroneously be included in `User.with_scope('admin')`.
+ #
+ # Rails uses YAML for serialization, so the markers are newlines. Unfortunately, sqlite can't match
+ # newlines reliably, and it doesn't natively support REGEXP. Therefore, hooks are currently being used
+ # to wrap roles in '!' markers when talking to the database. This is hacky, but unavoidable.
+ # The implication is that, for security, it must be actively enforced that role names cannot include
+ # the '!' character.
+ #
+ # An alternative would be to use JSON instead of YAML to serialize the data, but I've wrestled
+ # countless SerializationTypeMismatch errors trying to accomplish this, in vain. The real problem, of course,
+ # is even trying to query serialized data. I'm unsure how well this would work in different ruby versions or
+ # implementations, which may handle object dumping differently. Bitmasking seems to be a more reliable strategy.
+
+ base.class_eval do
+ const_set :ROLES_MARKER, '!'
+ scope :with_role, proc { |r|
+ query = "#{self.table_name}.#{column_name} LIKE " + ['"%',base::ROLES_MARKER,r,base::ROLES_MARKER,'%"'].join
+ where(query)
+ }
+
+ define_method :add_role_markers do
+ self[column_name.to_sym].map! { |r| [base::ROLES_MARKER,r,base::ROLES_MARKER].join }
+ end
+
+ define_method :strip_role_markers do
+ self[column_name.to_sym].map! { |r| r.gsub(base::ROLES_MARKER,'') }
+ end
+
+ private :add_role_markers, :strip_role_markers
+ before_save :add_role_markers
+ after_save :strip_role_markers
+ after_rollback :strip_role_markers
+ after_find :strip_role_markers
+ end
+
end
end
end
Oops, something went wrong.