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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/NOTES +98 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/adva_rbac.gemspec +17 -0
- data/app/controllers/roles_controller.rb +28 -0
- data/app/helpers/roles_helper.rb +64 -0
- data/app/views/admin/sections/settings/_permissions.html.erb +16 -0
- data/app/views/roles/index.js.erb +3 -0
- data/config/initializers/base_controller.rb +4 -0
- data/config/initializers/rbac.rb +60 -0
- data/config/initializers/user.rb +80 -0
- data/db/migrate/20080402000006_create_role_tables.rb +13 -0
- data/db/migrate/20090720132900_migrate_roles_table_to_new_rbac.rb +15 -0
- data/lib/action_controller/guards_permissions.rb +77 -0
- data/lib/adva_rbac.rb +18 -0
- data/lib/adva_rbac/version.rb +3 -0
- data/lib/permission_map.rb +70 -0
- data/lib/rbac.rb +26 -0
- data/lib/rbac/acts_as_role_context.rb +44 -0
- data/lib/rbac/acts_as_role_subject.rb +65 -0
- data/lib/rbac/context.rb +85 -0
- data/lib/rbac/role.rb +10 -0
- data/lib/rbac/role_type.rb +73 -0
- data/lib/rbac/role_type/active_record.rb +47 -0
- data/lib/rbac/role_type/static.rb +144 -0
- data/lib/rbac/subject.rb +52 -0
- data/test/functional/roles_controller_test.rb +21 -0
- data/test/integration/user_rbac_test.rb +34 -0
- data/test/rbac/all.rb +3 -0
- data/test/rbac/database.rb +155 -0
- data/test/rbac/database.yml +3 -0
- data/test/rbac/implementation/active_record_test.rb +17 -0
- data/test/rbac/implementation/static_test.rb +14 -0
- data/test/rbac/static.rb +25 -0
- data/test/rbac/test_helper.rb +62 -0
- data/test/rbac/tests/acts_as_role_context.rb +37 -0
- data/test/rbac/tests/context.rb +35 -0
- data/test/rbac/tests/group.rb +40 -0
- data/test/rbac/tests/has_role.rb +126 -0
- data/test/rbac/tests/role_type.rb +110 -0
- data/test/test_helper.rb +1 -0
- data/test/unit/helpers/roles_helper_test.rb +69 -0
- data/test/unit/models/rbac_context_test.rb +37 -0
- data/test/unit/models/rbac_user_test.rb +100 -0
- data/test/unit/models/role_test.rb +185 -0
- 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
|
data/lib/adva_rbac.rb
ADDED
@@ -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,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
|
data/lib/rbac.rb
ADDED
@@ -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)
|
data/lib/rbac/context.rb
ADDED
@@ -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
|
data/lib/rbac/role.rb
ADDED
@@ -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
|