adva_rbac 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|