Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #16 from jeriko/master

with_role feature
  • Loading branch information...
commit 17d4150727a689ebda69abb26091a83c284c8703 2 parents b5bac84 + 2d66aa5
Ryan Oberholzer 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
28 README.rdoc
@@ -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,12 +46,12 @@ 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.
@@ -59,7 +59,7 @@ And lastly you need to add a constant variable which stores an array of the diff
==== 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
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
17 lib/generators/active_record/easy_roles_generator.rb
@@ -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
View
13 lib/generators/active_record/templates/migration_bitmask.rb
@@ -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
View
15 lib/generators/active_record/templates/migration_non_bitmask.rb
@@ -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
View
12 lib/generators/easy_roles/easy_roles_generator.rb
@@ -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
View
16 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)
@@ -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
View
54 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)
+ 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,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
View
139 spec/easy_roles_spec.rb
@@ -31,17 +31,17 @@
end
it "should return the users role through association" do
- user = BitmaskUser.create(:name => "Bob")
+ user = BitmaskUser.create(name: 'Bob')
user.add_role! "admin"
- membership = Membership.create(:name => "Test Membership", :bitmask_user => user)
+ membership = Membership.create(name: 'Test Membership', bitmask_user: user)
Membership.last.bitmask_user.is_admin?.should be_true
end
it "should get no method error if no easy roles on model" do
begin
- b = Beggar.create(:name => "Ivor")
+ b = Beggar.create(name: 'Ivor')
b.is_admin?
rescue => e
@@ -51,8 +51,8 @@
it "should get no method error if no easy roles on model even through association" do
begin
- b = Beggar.create(:name => "Ivor")
- m = Membership.create(:name => "Beggars club", :beggar => b)
+ b = Beggar.create(name: 'Ivor')
+ m = Membership.create(name: 'Beggars club', beggar: b)
Membership.last.beggar.is_admin?
rescue => e
@@ -62,7 +62,7 @@
describe "normal methods" do
it "should not save to the database if not implicitly saved" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.reload
@@ -71,7 +71,7 @@
end
it "should save to the database if implicity saved" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -81,7 +81,7 @@
end
it "should clear all roles and not save if not implicitly saved" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -96,7 +96,7 @@
end
it "should clear all roles and save if implicitly saved" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -112,7 +112,7 @@
end
it "should remove a role and not save unless implicitly saved" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -127,7 +127,7 @@
end
it "should remove a role and save if implicitly saved" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -145,7 +145,7 @@
describe "bang method" do
it "should save to the database if the bang method is used" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role! 'admin'
user.is_admin?.should be_true
user.reload
@@ -154,7 +154,7 @@
end
it "should remove a role and save" do
- user = SerializeUser.create(:name => "Ryan")
+ user = SerializeUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -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
@@ -207,7 +267,7 @@
describe "normal methods" do
it "should not save to the database if not implicitly saved" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.reload
@@ -216,7 +276,7 @@
end
it "should save to the database if implicity saved" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -226,7 +286,7 @@
end
it "should clear all roles and not save if not implicitly saved" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -241,7 +301,7 @@
end
it "should clear all roles and save if implicitly saved" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -257,7 +317,7 @@
end
it "should remove a role and not save unless implicitly saved" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -272,7 +332,7 @@
end
it "should remove a role and save if implicitly saved" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -290,7 +350,7 @@
describe "bang method" do
it "should save to the database if the bang method is used" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role! 'admin'
user.is_admin?.should be_true
user.reload
@@ -299,7 +359,7 @@
end
it "should remove a role and save" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -314,7 +374,7 @@
end
it "should clear all roles and save" do
- user = BitmaskUser.create(:name => "Ryan")
+ user = BitmaskUser.create(name: 'Ryan')
user.add_role 'admin'
user.is_admin?.should be_true
user.save
@@ -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
+end
View
18 spec/spec_helper.rb
@@ -7,17 +7,17 @@
RSpec.configure do |config|
end
-ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
+ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
def setup_db
- ActiveRecord::Schema.define(:version => 1) do
+ ActiveRecord::Schema.define(version: 1) do
create_table :serialize_users do |t|
t.string :name
- t.string :roles, :default => "--- []"
+ t.string :roles, default: "--- []"
end
create_table :bitmask_users do |t|
t.string :name
- t.integer :roles_mask, :default => 0
+ t.integer :roles_mask, default: 0
end
create_table :memberships do |t|
@@ -41,12 +41,16 @@ def teardown_db
setup_db
class SerializeUser < ActiveRecord::Base
- easy_roles :roles, :method => :serialize
+ 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
+ easy_roles :roles_mask, method: :bitmask
ROLES_MASK = %w[admin manager user]
end
@@ -58,4 +62,4 @@ class Membership < ActiveRecord::Base
class Beggar < ActiveRecord::Base
has_many :memberships
-end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.