adva_rbac 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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