Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'feature/scopes'

  • Loading branch information...
commit 63e87df773771198cdcf6a2634d8524d55b2cf60 2 parents 4351290 + 465a1f0
@jeriko jeriko authored
View
3  Gemfile.lock
@@ -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
8 lib/easy_roles.rb
@@ -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
View
3  lib/generators/active_record/easy_roles_generator.rb
@@ -9,6 +9,9 @@ class EasyRolesGenerator < ActiveRecord::Generators::Base
class_option :use_bitmask_method, type: :boolean, required: false, default: false,
desc: 'Setup migration for Bitmask method'
+ class_option :add_index, type: :boolean, required: false, default: false,
+ desc: 'Add an index to the relevant column'
+
desc 'Internal use by easy_roles generator - use that instead'
source_root File.expand_path('../templates', __FILE__)
View
3  lib/generators/active_record/templates/migration_bitmask.rb
@@ -2,6 +2,9 @@ class AddBitmaskRolesTo<%= table_name.camelize %> < ActiveRecord::Migration
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
View
5 lib/generators/active_record/templates/migration_non_bitmask.rb
@@ -2,6 +2,9 @@ class AddEasyRolesTo<%= table_name.camelize %> < ActiveRecord::Migration
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
-end
+end
View
13 lib/methods/bitmask.rb
@@ -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
56 lib/methods/serialize.rb
@@ -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
@@ -14,12 +15,18 @@ def initialize(base, column_name, options)
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,49 @@ 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, '!'
+ 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
View
93 spec/easy_roles_spec.rb
@@ -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 = ''
+ (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?(chuck).should be_true
+ 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::ROLES_MARKER = '!'
+ user = SerializeUser.create(name: 'Towelie')
+ user.add_role!('funkytown!').should be_false
+ SerializeUser::ROLES_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
4 spec/spec_helper.rb
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.