adva_rbac 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/NOTES +98 -0
  6. data/README.md +29 -0
  7. data/Rakefile +2 -0
  8. data/adva_rbac.gemspec +17 -0
  9. data/app/controllers/roles_controller.rb +28 -0
  10. data/app/helpers/roles_helper.rb +64 -0
  11. data/app/views/admin/sections/settings/_permissions.html.erb +16 -0
  12. data/app/views/roles/index.js.erb +3 -0
  13. data/config/initializers/base_controller.rb +4 -0
  14. data/config/initializers/rbac.rb +60 -0
  15. data/config/initializers/user.rb +80 -0
  16. data/db/migrate/20080402000006_create_role_tables.rb +13 -0
  17. data/db/migrate/20090720132900_migrate_roles_table_to_new_rbac.rb +15 -0
  18. data/lib/action_controller/guards_permissions.rb +77 -0
  19. data/lib/adva_rbac.rb +18 -0
  20. data/lib/adva_rbac/version.rb +3 -0
  21. data/lib/permission_map.rb +70 -0
  22. data/lib/rbac.rb +26 -0
  23. data/lib/rbac/acts_as_role_context.rb +44 -0
  24. data/lib/rbac/acts_as_role_subject.rb +65 -0
  25. data/lib/rbac/context.rb +85 -0
  26. data/lib/rbac/role.rb +10 -0
  27. data/lib/rbac/role_type.rb +73 -0
  28. data/lib/rbac/role_type/active_record.rb +47 -0
  29. data/lib/rbac/role_type/static.rb +144 -0
  30. data/lib/rbac/subject.rb +52 -0
  31. data/test/functional/roles_controller_test.rb +21 -0
  32. data/test/integration/user_rbac_test.rb +34 -0
  33. data/test/rbac/all.rb +3 -0
  34. data/test/rbac/database.rb +155 -0
  35. data/test/rbac/database.yml +3 -0
  36. data/test/rbac/implementation/active_record_test.rb +17 -0
  37. data/test/rbac/implementation/static_test.rb +14 -0
  38. data/test/rbac/static.rb +25 -0
  39. data/test/rbac/test_helper.rb +62 -0
  40. data/test/rbac/tests/acts_as_role_context.rb +37 -0
  41. data/test/rbac/tests/context.rb +35 -0
  42. data/test/rbac/tests/group.rb +40 -0
  43. data/test/rbac/tests/has_role.rb +126 -0
  44. data/test/rbac/tests/role_type.rb +110 -0
  45. data/test/test_helper.rb +1 -0
  46. data/test/unit/helpers/roles_helper_test.rb +69 -0
  47. data/test/unit/models/rbac_context_test.rb +37 -0
  48. data/test/unit/models/rbac_user_test.rb +100 -0
  49. data/test/unit/models/role_test.rb +185 -0
  50. metadata +110 -0
@@ -0,0 +1,13 @@
1
+ class CreateRoleTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :roles do |t|
4
+ t.references :user # TODO reference a membership instead?
5
+ t.references :context, :polymorphic => true
6
+ t.string :type, :limit => 25
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :roles
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class MigrateRolesTableToNewRbac < ActiveRecord::Migration
2
+ def self.up
3
+ rename_column :roles, :type, :name
4
+ add_column :roles, :ancestor_context_id, :integer
5
+ add_column :roles, :ancestor_context_type, :string
6
+ Rbac::Role.scoped.each { |role| role.update_attribute(:name, role.name.demodulize.underscore) }
7
+ end
8
+
9
+ def self.down
10
+ Role.scoped.each { |role| role.update_attribute(:name, "Rbac::Role::#{role.name.camelize}") }
11
+ remove_column :roles, :ancestor_context_id
12
+ remove_column :roles, :ancestor_context_type
13
+ rename_column :roles, :name, :type
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ # TODO move to some rails specific adapter in Rbac?
2
+
3
+ module ActionController
4
+ class RoleRequired < SecurityError
5
+ attr_accessor :required_role
6
+ def initialize(role_types, action, type)
7
+ @required_roles = role_types
8
+ @action = action
9
+ @type = type
10
+ super I18n.t(:'adva.roles.errors.messages.role_required', :roles => role_types)
11
+ end
12
+ end
13
+
14
+ module GuardsPermissions
15
+ def self.included(base)
16
+ base.extend ClassMethods
17
+ end
18
+
19
+ module ClassMethods
20
+ def guards_permissions(type, options = {})
21
+ return if guards_permissions?
22
+ include InstanceMethods
23
+ extend ClassMethods
24
+
25
+ helper_method :has_permission?
26
+
27
+ class_attribute :action_map
28
+ set_action_map options.except(:only, :except)
29
+
30
+ before_filter(options.slice(:only, :except)) do |controller|
31
+ controller.guard_permission(type)
32
+ end
33
+ end
34
+
35
+ def guards_permissions?
36
+ included_modules.include? InstanceMethods
37
+ end
38
+ end
39
+
40
+ module ClassMethods
41
+ # maps controller actions to (virtual) model actions that are referenced
42
+ # by the roles system
43
+ def set_action_map(map)
44
+ self.action_map = { :index => :show, :edit => :update, :new => :create }
45
+ map.each do |target, actions|
46
+ Array(actions).each{|action| self.action_map[action] = target }
47
+ end
48
+ end
49
+ end
50
+
51
+ module InstanceMethods
52
+ def guard_permission(*args)
53
+ type = args.pop
54
+ action = args.pop || map_from_controller_action
55
+
56
+ # TODO :show permissions do not make any sense right now, so we
57
+ # completely deactivate them
58
+ # return if action.to_sym == :show
59
+
60
+ unless has_permission?(action, type)
61
+ role_types = current_resource.authorizing_role_types_for(:"#{action} #{type}")
62
+ raise RoleRequired.new(role_types, action, type)
63
+ end
64
+ end
65
+
66
+ def has_permission?(action, type)
67
+ action = :"#{action} #{type}"
68
+ user = current_user || User.anonymous
69
+ user.has_permission?(action, current_resource)
70
+ end
71
+
72
+ def map_from_controller_action
73
+ action_map[params[:action].to_sym] || params[:action].to_sym
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,18 @@
1
+ # require "adva_rbac/version"
2
+ require "rails"
3
+
4
+ require "rbac"
5
+ require "rbac/role_type/static"
6
+ # require "active_record/acts_as_role_context"
7
+ require "action_controller/guards_permissions"
8
+
9
+ module AdvaRbac
10
+ class Engine < Rails::Engine
11
+ initializer "adva_rbac.init" do
12
+ ActiveRecord::Base.send :include, Rbac::ActsAsRoleContext
13
+ ActionController::Base.send :include, ActionController::GuardsPermissions
14
+
15
+ Rbac::RoleType.implementation = Rbac::RoleType::Static
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module AdvaRbac
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,70 @@
1
+ # TODO - muuuuch of this could be done so much nicer
2
+
3
+
4
+ # Manages permissions that map actions to roles.
5
+ #
6
+ # Ensures the given actions are arrays, invert the role => action hash
7
+ # to a action => role hash while expanding the actions array to keys,
8
+ # and expand the action key :all to the default actions.
9
+ #
10
+ # E.g.:
11
+ #
12
+ # :comment => {:user => :create, :admin => [:edit, :delete]}
13
+ #
14
+ # becomes:
15
+ #
16
+ # :comment => {:create => :user, :edit => :admin, :delete => :admin}
17
+
18
+ class PermissionMap < Hash
19
+ def default_actions
20
+ [:show, :create, :update, :destroy]
21
+ end
22
+
23
+ def initialize(permissions)
24
+ permissions.clone.each do |type, roles|
25
+ roles.each do |role, actions|
26
+ actions = actions == :all ? default_actions : Array(actions)
27
+ set type, Hash[*(actions.zip [role] * actions.size).flatten]
28
+ end
29
+ end
30
+ end
31
+
32
+ def set(type, permissions)
33
+ self[type] ||= {}
34
+ self[type].update permissions
35
+ end
36
+
37
+ def sorted
38
+ sorted = ActiveSupport::OrderedHash.new
39
+ sorted_keys.each do |type|
40
+ roles = self[type]
41
+ # put default_actions to the front, sort the rest
42
+ # then only use keys that were present in the original key set
43
+ keys = default_actions + sorted_keys(roles.keys - default_actions) & roles.keys
44
+ (keys).each do |key|
45
+ sorted[type] ||= ActiveSupport::OrderedHash.new
46
+ sorted[type][key] = self[type][key]
47
+ end
48
+ end
49
+ sorted
50
+ end
51
+
52
+ def sorted_keys(keys = nil)
53
+ keys ||= self.keys
54
+ keys.map(&:to_s).sort.map(&:to_sym)
55
+ end
56
+
57
+ def update(other)
58
+ merged = self.dup
59
+ other.each do |type, permissions|
60
+ permissions.each do |action, role|
61
+ type = type.to_sym
62
+ action = action.to_sym
63
+ self[type] ||= {}
64
+ self[type][action] ||= {}
65
+ self[type][action] = role.to_sym
66
+ end
67
+ end
68
+ merged
69
+ end
70
+ end
@@ -0,0 +1,26 @@
1
+ require 'rbac/acts_as_role_context'
2
+ require 'rbac/acts_as_role_subject'
3
+ require 'rbac/context'
4
+ require 'rbac/subject'
5
+ require 'rbac/role_type'
6
+ require 'rbac/role'
7
+
8
+ module Rbac
9
+ class UndefinedRoleType < IndexError
10
+ def initialize(name)
11
+ super "Could not find role type named #{name}"
12
+ end
13
+ end
14
+
15
+ class AuthorizingRoleNotFound < IndexError
16
+ def initialize(context, action)
17
+ super "Could not find role(s) for #{action} (on: #{context.inspect})"
18
+ end
19
+ end
20
+
21
+ class NoImplementation < RuntimeError
22
+ def initialize
23
+ super "No implementation configured: Rbac::RoleType.implementation"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ module Rbac
2
+ module ActsAsRoleContext
3
+ def self.included(base)
4
+ base.extend ActMacro
5
+ end
6
+
7
+ module ActMacro
8
+ def acts_as_role_context(options = {})
9
+ return if acts_as_role_context?
10
+
11
+ include InstanceMethods
12
+
13
+ serialize :permissions
14
+ cattr_accessor :role_context_class
15
+
16
+ self.role_context_class = begin
17
+ "#{self.name}::RoleContext".constantize
18
+ rescue NameError
19
+ Rbac::Context.define_class(self, options)
20
+ end
21
+ end
22
+
23
+ def acts_as_role_context?
24
+ included_modules.include?(Rbac::ActsAsRoleContext::InstanceMethods)
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ delegate :authorizing_role_types_for, :to => :role_context
30
+
31
+ # returns the role context wrapper associated to the domain object (e.g. Site)
32
+ def role_context
33
+ @role_context ||= self.role_context_class.new(self)
34
+ end
35
+
36
+ # attribute reader that returns a hash as a default
37
+ def permissions
38
+ read_attribute(:permissions) || {}
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ ActiveRecord::Base.send(:include, Rbac::ActsAsRoleContext)
@@ -0,0 +1,65 @@
1
+ module Rbac
2
+ module ActsAsRoleSubject
3
+ def self.included(base)
4
+ base.extend ActMacro
5
+ end
6
+
7
+ module ActMacro
8
+ def acts_as_role_subject(options = {})
9
+ return if acts_as_role_subject?
10
+
11
+ include InstanceMethods
12
+
13
+ serialize :permissions
14
+ cattr_accessor :role_subject_class
15
+
16
+ self.role_subject_class = begin
17
+ "#{self.name}::RoleSubject".constantize
18
+ rescue NameError
19
+ Rbac::Subject.define_class(self, options)
20
+ end
21
+ end
22
+
23
+ def acts_as_role_subject?
24
+ included_modules.include?(Rbac::ActsAsRoleContext::InstanceMethods)
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ # returns the role subject wrapper associated to the domain object (e.g. User)
30
+ def role_subject
31
+ @role_subject ||= self.role_subject_class.new(self)
32
+ end
33
+
34
+ def has_role?(*args)
35
+ role_subject.has_role?(*args)
36
+ end
37
+
38
+ def has_global_role?(type, site = nil)
39
+ return self.roles.any? do |role|
40
+ (role.name == "superuser" && type == :superuser ||
41
+ role.name == type.to_s && role.context_type == "Site" && role.context_id == site.id)
42
+ end
43
+ end
44
+
45
+ def has_permission_for_admin_area?(site)
46
+ permitted_roles = [:superuser, :admin, :moderator, :author, :designer]
47
+ return self.roles.any? do |role|
48
+ permitted_roles.any? do |permitted_role|
49
+ self.has_global_role?(permitted_role, site)
50
+ end
51
+ end
52
+ end
53
+
54
+ def has_permission?(*args)
55
+ role_subject.has_permission?(*args)
56
+ end
57
+
58
+ def has_explicit_role?(*args)
59
+ role_subject.has_explicit_role?(*args)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ ActiveRecord::Base.send(:include, Rbac::ActsAsRoleSubject)
@@ -0,0 +1,85 @@
1
+ module Rbac
2
+ class Context
3
+ mattr_accessor :default_permissions
4
+ self.default_permissions = {}
5
+
6
+ class << self
7
+ def root
8
+ @root ||= Base.new(self)
9
+ end
10
+
11
+ def define_class(model, options)
12
+ Class.new(Base).tap do |klass|
13
+ model.const_set('RoleContext', klass).class_eval do
14
+ self.parent_accessor = options.delete(:parent)
15
+ self.options = options
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ class Base
22
+ class_attribute :parent_accessor, :options, :children
23
+ self.options = {}
24
+ self.children = []
25
+
26
+ attr_accessor :object
27
+
28
+ def initialize(object = nil)
29
+ self.object = object
30
+ end
31
+
32
+ def authorizing_role_types_for(action)
33
+ raise ArgumentError.new("No action given (on: #{self.inspect})") unless action
34
+ result = self_and_parents.inject([]) do |types, context|
35
+ types += Array(context.permissions[action.to_sym])
36
+ end
37
+ raise(AuthorizingRoleNotFound.new(self, action)) if result.empty?
38
+ result
39
+ end
40
+
41
+ def expand_roles_for(action)
42
+ types = build_role_types_for(action)
43
+ contexts = self_and_parents - [Rbac::Context.root]
44
+
45
+ contexts.collect do |context|
46
+ types.collect { |type| type.expand(context.object) }
47
+ end.flatten.uniq
48
+ end
49
+
50
+ def include?(context)
51
+ return false unless context
52
+ context = context.role_context unless context.is_a?(Base)
53
+ begin
54
+ return true if self.object == context.object
55
+ end while context = context.parent
56
+ false
57
+ end
58
+
59
+ def self_and_parents
60
+ [self] + (parent ? parent.self_and_parents : [])
61
+ end
62
+
63
+ def parent
64
+ if parent_accessor and parent = object.send(parent_accessor)
65
+ parent.role_context
66
+ elsif self != Rbac::Context.root
67
+ Rbac::Context.root # might want to return a fake domain model here
68
+ end
69
+ end
70
+
71
+ protected
72
+
73
+ def permissions
74
+ return Rbac::Context.default_permissions if self.class == Rbac::Context::Base
75
+ @permissions ||= (object.try(:permissions) || {}).symbolize_keys
76
+ end
77
+
78
+ def build_role_types_for(action)
79
+ authorizing_role_types_for(action).collect do |type|
80
+ Rbac::RoleType.build(type).self_and_masters
81
+ end.flatten.compact
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,10 @@
1
+ module Rbac
2
+ class Role < ActiveRecord::Base
3
+ belongs_to :subject, :polymorphic => true
4
+ belongs_to :context, :polymorphic => true
5
+
6
+ def has_context?
7
+ context_type && context_id
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,73 @@
1
+ module Rbac
2
+ module RoleType
3
+ mattr_accessor :implementation
4
+
5
+ class << self
6
+ def build(*args)
7
+ implementation.build(*args)
8
+ end
9
+
10
+ def types
11
+ implementation.all
12
+ end
13
+
14
+ def implementation
15
+ @@implementation || raise(NoImplementation.new)
16
+ end
17
+ end
18
+
19
+ def name
20
+ super.split('::').last.underscore
21
+ end
22
+
23
+ def expand(object)
24
+ expansion = [name]
25
+ expansion += [object.class.to_s.underscore, object.id] if requires_context?
26
+ expansion.join('-')
27
+ end
28
+
29
+ def requires_context?
30
+ true
31
+ end
32
+
33
+ def self_and_masters
34
+ [self] + all_masters
35
+ end
36
+
37
+ def masters
38
+ []
39
+ end
40
+
41
+ def all_masters
42
+ masters + masters.map(&:all_masters).flatten.uniq
43
+ end
44
+
45
+ def self_and_minions
46
+ [self] + all_minions
47
+ end
48
+
49
+ def minions
50
+ []
51
+ end
52
+
53
+ def all_minions
54
+ minions + minions.map(&:all_minions).flatten.uniq
55
+ end
56
+
57
+ def minion_of?(name)
58
+ self_and_masters.any? { |type| type.name == name }
59
+ end
60
+
61
+ def included_in?(role, context = nil)
62
+ minion_of?(role.name) && (!role.context || role.context.role_context.include?(context))
63
+ end
64
+
65
+ def granted_to?(subject, context = nil, options = {})
66
+ # this implementaion makes the assumption that subject implements an roles
67
+ # method returning objects that carry the role's type/name and context
68
+ !!subject.roles.detect do |role|
69
+ options[:explicit] ? self.name == role.name : self.included_in?(role, context)
70
+ end
71
+ end
72
+ end
73
+ end