Permalink
Browse files

Implemented `with_scope` ActiveRecord::Relations for Bitmask and Seri…

…alize strategies.
  • Loading branch information...
1 parent f6f84f5 commit ecae1a0aeb552c94422f90158102af2b19716215 @jeriko jeriko committed Nov 26, 2011
Showing with 158 additions and 2 deletions.
  1. +1 −1 lib/easy_roles.rb
  2. +13 −0 lib/methods/bitmask.rb
  3. +47 −1 lib/methods/serialize.rb
  4. +93 −0 spec/easy_roles_spec.rb
  5. +4 −0 spec/spec_helper.rb
View
@@ -15,7 +15,7 @@ module EasyRoles
end
module ClassMethods
- def easy_roles(name, options = {})
+ def easy_roles(name, options = {})
begin
raise NameError unless ALLOWED_METHODS.include? options[:method]
rescue NameError
View
@@ -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)
@@ -51,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
View
@@ -1,12 +1,13 @@
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)
end
-
+
base.send :define_method, :has_role? do |role|
self[column_name.to_sym].include?(role)
end
@@ -18,6 +19,7 @@ def initialize(base, column_name, options)
end
base.send :define_method, :add_role! do |role|
+ return false if !base::ROLES_MARKER.empty? && role.include?(base::ROLES_MARKER)
add_role(role)
self.save!
end
@@ -40,6 +42,50 @@ def initialize(base, column_name, options)
end
base.send :private, :make_default_roles
+
+
+ # Scopes (Ugly, no cross-table query support, potentially unsafe. Fix?)
+ # ----------------------------------------------------------------------------------------------------
+ # 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.
+ #
+ # Adding a dependancy to something like Squeel would allow for cleaner syntax in the `where()`, with the
+ # added bonus of supporting complex cross-table queries. 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.
+
+ base.class_eval do
+ const_set :ROLES_MARKER, '!'
+
+ 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
+
+ scope :with_role, proc { |r|
+ query = "#{self.table_name}.#{column_name} LIKE " + ['"%',base::ROLES_MARKER,r,base::ROLES_MARKER,'%"'].join
+ where(query)
+ }
+ end
+
end
end
end
View
@@ -168,6 +168,66 @@
user.is_admin?.should be_false
end
end
+
+ describe "scopes" do
+ describe "with_role" do
+ it "should implement the `with_role` scope" do
+ SerializeUser.respond_to?(:with_role).should be_true
+ end
+
+ it "should return an ActiveRecord::Relation" do
+ SerializeUser.with_role('admin').class.should == ActiveRecord::Relation
+ end
+
+ it "should match records for a given role" do
+ user = SerializeUser.create(name: 'Daniel')
+ SerializeUser.with_role('admin').include?(user).should be_false
+ user.add_role! 'admin'
+ SerializeUser.with_role('admin').include?(user).should be_true
+ end
+
+ it "should be chainable" do
+ (daniel = SerializeUser.create(name: 'Daniel')).add_role! 'user'
+ (ryan = SerializeUser.create(name: 'Ryan')).add_role! 'user'
+ ryan.add_role! 'admin'
+ admin_users = SerializeUser.with_role('user').with_role('admin')
+ admin_users.include?(ryan).should be_true
+ admin_users.include?(daniel).should be_false
+ end
+
+ it "should prove that wrapper markers are a necessary strategy by failing without them" do
+ marker_cache = SerializeUser::ROLES_MARKER
+ SerializeUser::ROLES_MARKER = ''
+ (chuck = SerializeUser.create(name: 'Mr. Norris')).add_role!('recursion')
+ (morgan = SerializeUser.create(name: 'Mr. Freeman')).add_role!('onrecursionrecursi')
+ SerializeUser.with_role('recursion').include?(morgan).should be_true
+ SerializeUser::ROLES_MARKER = marker_cache
+ end
+
+ it "should avoid incorrectly matching roles where the name is a subset of another role's name" do
+ (chuck = SerializeUser.create(name: 'Mr. Norris')).add_role!('recursion')
+ (morgan = SerializeUser.create(name: 'Mr. Freeman')).add_role!('onrecursionrecursi')
+ SerializeUser.with_role('recursion').include?(morgan).should be_false
+ end
+
+ it "should not allow roles to be added if they include the ROLES_MARKER character" do
+ marker_cache = SerializeUser::ROLES_MARKER
+ SerializeUser::MARKER = '!'
+ user = SerializeUser.create(name: 'Towelie')
+ user.add_role!('funkytown!').should be_false
+ SerializeUser::MARKER = marker_cache
+ end
+
+ it "should correctly handle markers on failed saves" do
+ the_king = UniqueSerializeUser.create(name: 'Elvis')
+ (imposter = UniqueSerializeUser.create(name: 'Elvisbot')).add_role!('sings-like-a-robot')
+ imposter.name = 'Elvis'
+ imposter.save.should be_false
+ imposter.roles.any? {|r| r.include?(SerializeUser::ROLES_MARKER) }.should be_false
+ end
+
+ end
+ end
end
describe "bitmask method" do
@@ -328,5 +388,38 @@
user.is_admin?.should be_false
end
end
+
+ describe "scopes" do
+ describe "with_role" do
+ it "should implement the `with_role` scope" do
+ BitmaskUser.respond_to?(:with_role).should be_true
+ end
+
+ it "should return an ActiveRecord::Relation" do
+ BitmaskUser.with_role('admin').class.should == ActiveRecord::Relation
+ end
+
+ it "should raise an ArgumentError for undefined roles" do
+ expect { BitmaskUser.with_role('your_mom') }.should raise_error(ArgumentError)
+ end
+
+ it "should match records with a given role" do
+ user = BitmaskUser.create(name: 'Daniel')
+ BitmaskUser.with_role('admin').include?(user).should be_false
+ user.add_role! 'admin'
+ BitmaskUser.with_role('admin').include?(user).should be_true
+ end
+
+ it "should be chainable" do
+ (daniel = BitmaskUser.create(name: 'Daniel')).add_role! 'user'
+ (ryan = BitmaskUser.create(name: 'Ryan')).add_role! 'user'
+ ryan.add_role! 'admin'
+ admin_users = BitmaskUser.with_role('user').with_role('admin')
+ admin_users.include?(ryan).should be_true
+ admin_users.include?(daniel).should be_false
+ end
+ end
+ end
+
end
end
View
@@ -44,6 +44,10 @@ class SerializeUser < ActiveRecord::Base
easy_roles :roles, method: :serialize
end
+class UniqueSerializeUser < SerializeUser
+ validates :name, uniqueness: true
+end
+
class BitmaskUser < ActiveRecord::Base
has_many :memberships
easy_roles :roles_mask, method: :bitmask

0 comments on commit ecae1a0

Please sign in to comment.