skill_tree 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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/lib/generators/skill_tree/install_generator.rb +20 -0
  3. data/lib/generators/skill_tree/orm_helpers.rb +14 -0
  4. data/lib/generators/skill_tree/templates/acl.rb +42 -0
  5. data/lib/generators/skill_tree/templates/create_skill_tree_acl_system.rb +48 -0
  6. data/lib/generators/skill_tree/templates/initializer.rb +1 -0
  7. data/lib/skill_tree.rb +15 -0
  8. data/lib/skill_tree/controller.rb +57 -0
  9. data/lib/skill_tree/errors.rb +5 -0
  10. data/lib/skill_tree/models.rb +10 -0
  11. data/lib/skill_tree/models/acl.rb +13 -0
  12. data/lib/skill_tree/models/acl_mapping.rb +16 -0
  13. data/lib/skill_tree/models/acl_ownership.rb +11 -0
  14. data/lib/skill_tree/models/permission.rb +10 -0
  15. data/lib/skill_tree/models/role.rb +22 -0
  16. data/lib/skill_tree/models/user_role.rb +14 -0
  17. data/lib/skill_tree/parser.rb +44 -0
  18. data/lib/skill_tree/parser/acl_parser.rb +42 -0
  19. data/lib/skill_tree/parser/role_parser.rb +57 -0
  20. data/lib/skill_tree/resource.rb +21 -0
  21. data/lib/skill_tree/resource/callbacks.rb +20 -0
  22. data/lib/skill_tree/resource/class_methods.rb +10 -0
  23. data/lib/skill_tree/resource/instance_methods.rb +52 -0
  24. data/lib/skill_tree/resource/relations.rb +18 -0
  25. data/lib/skill_tree/resource/scopes.rb +38 -0
  26. data/lib/skill_tree/subject.rb +53 -0
  27. data/lib/skill_tree/version.rb +3 -0
  28. data/spec/db/schema.rb +59 -0
  29. data/spec/skill_tree/controller_spec.rb +103 -0
  30. data/spec/skill_tree/generators/install_generator_spec.rb +34 -0
  31. data/spec/skill_tree/models/acl_mapping_spec.rb +46 -0
  32. data/spec/skill_tree/models/acl_ownership_spec.rb +30 -0
  33. data/spec/skill_tree/models/acl_spec.rb +32 -0
  34. data/spec/skill_tree/models/permission_spec.rb +16 -0
  35. data/spec/skill_tree/models/role_spec.rb +41 -0
  36. data/spec/skill_tree/models/user_role_spec.rb +16 -0
  37. data/spec/skill_tree/parser_spec.rb +71 -0
  38. data/spec/skill_tree/resource_spec.rb +137 -0
  39. data/spec/skill_tree/subject_spec.rb +65 -0
  40. data/spec/skill_tree_spec.rb +8 -0
  41. data/spec/spec_helper.rb +37 -0
  42. data/spec/support/acls/acl_example.rb +19 -0
  43. data/spec/support/acls/acl_example2.rb +24 -0
  44. data/spec/support/acls/acl_example3.rb +24 -0
  45. data/spec/support/acls/acl_example4.rb +24 -0
  46. data/spec/support/acls/subject_acl.rb +57 -0
  47. data/spec/support/capture_stdout.rb +12 -0
  48. data/spec/support/controllers.rb +71 -0
  49. data/spec/support/model_builder.rb +15 -0
  50. data/spec/support/models.rb +16 -0
  51. metadata +232 -0
@@ -0,0 +1,21 @@
1
+ require 'skill_tree/resource/callbacks.rb'
2
+ require 'skill_tree/resource/class_methods.rb'
3
+ require 'skill_tree/resource/instance_methods.rb'
4
+ require 'skill_tree/resource/relations.rb'
5
+ require 'skill_tree/resource/scopes.rb'
6
+
7
+ module SkillTree
8
+ module Resource
9
+ module Initializer
10
+ def as_skill_tree_resource(options = {})
11
+ send :include, SkillTree::Resource::InstanceMethods
12
+ send :include, SkillTree::Resource::Relations
13
+ send :include, SkillTree::Resource::Scopes
14
+ send :include, SkillTree::Resource::Callbacks
15
+ send :extend, SkillTree::Resource::ClassMethods
16
+ send :skill_tree_options=, options
17
+ end
18
+ end
19
+ end
20
+ end
21
+ ActiveRecord::Base.send :extend, SkillTree::Resource::Initializer
@@ -0,0 +1,20 @@
1
+ module SkillTree
2
+ module Resource
3
+ module Callbacks
4
+ def self.included(base)
5
+ base.before_save do |model|
6
+ unless model.acl
7
+ acl = SkillTree::Parser::Initializer.default_acl_for(model)
8
+ model.acl = acl if acl
9
+ end
10
+ end
11
+ base.after_save :skill_tree_setup_resource_owner
12
+ end
13
+
14
+ def skill_tree_setup_resource_owner
15
+ admin = skill_tree_options[:admin]
16
+ send(admin).role!(:admin, self) if admin && acl && try(admin)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ module SkillTree
2
+ module Resource
3
+ module ClassMethods
4
+ def self.extended(base)
5
+ base.send :cattr_accessor, :skill_tree_options
6
+ base.send :class_variable_set, :@@skill_tree_options, {}
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,52 @@
1
+ module SkillTree
2
+ module Resource
3
+ module InstanceMethods
4
+ def acl
5
+ acl_ownerships.first.acl if acl_ownerships.first
6
+ end
7
+
8
+ def acl=(value)
9
+ if acl_ownerships.empty?
10
+ acl_ownerships.new(acl: value)
11
+ else
12
+ fail SkillTree::AclAlreadySet,
13
+ "#{self.class.name} already has an acl. Use .acl! instead."
14
+ end
15
+ end
16
+
17
+ def acl!(value)
18
+ new_acl = parse_acl(value)
19
+ ActiveRecord::Base.transaction do
20
+ acl_ownerships.destroy_all
21
+ acl_ownerships.create!(acl: new_acl)
22
+ end if new_acl != acl
23
+ end
24
+
25
+ def permissions_for(user)
26
+ role = SkillTree::Models::Role.find_for(user, self)
27
+ SkillTree::Models::Permission.joins(
28
+ acl_mappings: [:role, :acl])
29
+ .where('("roles"."id" = ?)', role)
30
+ .where('("acls"."id" = ?)', acl)
31
+ end
32
+
33
+ def default_permission?(action, role = :user)
34
+ acls.joins(acl_mappings: [:permission, :role])
35
+ .where('"permissions"."name" = ?', action.to_s)
36
+ .where('"roles"."name" = ?', role.to_s).any?
37
+ end
38
+
39
+ alias_method :has_default_permission?, :default_permission?
40
+
41
+ private
42
+
43
+ def parse_acl(value)
44
+ if value.is_a? ActiveRecord::Base
45
+ value
46
+ else
47
+ SkillTree::Models::Acl.find_by!(name: value.to_s)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ module SkillTree
2
+ module Resource
3
+ module Relations
4
+ def self.included(base)
5
+ base.has_many :acl_ownerships,
6
+ as: :resource,
7
+ class_name: 'SkillTree::Models::AclOwnership'
8
+ base.has_many :acls,
9
+ through: :acl_ownerships,
10
+ class_name: 'SkillTree::Models::Acl'
11
+ base.has_many :roles, through: :acls,
12
+ class_name: 'SkillTree::Models::Role'
13
+ base.has_many :user_roles,
14
+ as: :resource, class_name: 'SkillTree::Models::UserRole'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ module SkillTree
2
+ module Resource
3
+ module Scopes
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ scope :where_user_can, lambda { |user, action|
7
+ if user
8
+ where_logged_user_can(action, user.id)
9
+ else
10
+ where_guest_can(action)
11
+ end
12
+ }
13
+
14
+ scope :with_permissions, lambda {
15
+ joins(acls: { acl_mappings: [:permission, :role] })
16
+ }
17
+
18
+ scope :where_guest_can, lambda { |action|
19
+ with_permissions
20
+ .where('"permissions"."name" = ?', action.to_s)
21
+ .where('"roles"."name" = ?', 'guest')
22
+ }
23
+
24
+ scope :where_logged_user_can, lambda { |action, user_id|
25
+ with_permissions
26
+ .joins("LEFT OUTER JOIN \"user_roles\""\
27
+ " ON \"user_roles\".\"resource_id\" = \"#{table_name}\".\"id\""\
28
+ " AND \"user_roles\".\"resource_type\" = '#{name}'")
29
+ .where('"permissions"."name" = ?', action.to_s)
30
+ .where('("user_roles"."role_id" = "roles"."id"'\
31
+ ' AND "user_roles"."user_id" = ?) OR ("roles"."name" = ?)',
32
+ user_id, 'user')
33
+ .uniq
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,53 @@
1
+ module SkillTree
2
+ module Subject
3
+ module Initializer
4
+ def as_skill_tree_subject
5
+ send :include, SkillTree::Subject::InstanceMethods
6
+ end
7
+ end
8
+ module InstanceMethods
9
+ def self.included(base)
10
+ base.send :has_many, :user_roles,
11
+ class_name: 'SkillTree::Models::UserRole'
12
+ base.send :has_many, :roles,
13
+ through: :user_roles, class_name: 'SkillTree::Models::Role'
14
+ end
15
+
16
+ def can?(action, resource)
17
+ resource.default_permission?(action) ||
18
+ permitted_with_role?(action, resource)
19
+ end
20
+ alias_method :allowed_to?, :can?
21
+
22
+ def role?(role_name, resource)
23
+ user_roles.joins(:role)
24
+ .where(roles: { name: role_name.to_s })
25
+ .where(resource: resource).any?
26
+ end
27
+
28
+ def role!(role, resource)
29
+ role = SkillTree::Models::Role.find_by_name!(role)
30
+ user_role = user_roles.find_or_initialize_by(resource: resource)
31
+ user_role.update!(role: role)
32
+ end
33
+
34
+ def unrole!(role, resource)
35
+ role = SkillTree::Models::Role.find_by_name!(role)
36
+ active_roles = user_roles.where(role: role, resource: resource)
37
+ active_roles.destroy_all
38
+ end
39
+
40
+ alias_method :has_role?, :role?
41
+
42
+ private
43
+
44
+ def permitted_with_role?(action, resource)
45
+ user_roles.joins(role: { acl_mappings: [:permission, :acl] })
46
+ .where('"permissions"."name" = ?', action.to_s)
47
+ .where('"acls"."id" = ?', resource.acl)
48
+ .where(resource: resource).any?
49
+ end
50
+ end
51
+ end
52
+ end
53
+ ActiveRecord::Base.send :extend, SkillTree::Subject::Initializer
@@ -0,0 +1,3 @@
1
+ module SkillTree
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,59 @@
1
+ ActiveRecord::Schema.define(version: 0) do
2
+ create_table :users do |t|
3
+ t.string :name
4
+ end
5
+
6
+ create_table :posts do |t|
7
+ t.string :text
8
+ end
9
+
10
+ create_table :post_with_authors do |t|
11
+ t.string :text
12
+ t.references :user
13
+ end
14
+
15
+ create_table :acls do |t|
16
+ t.string :name, null: false
17
+ t.integer :version, null: false
18
+ end
19
+ add_index :acls, :name, unique: true
20
+
21
+ create_table :roles do |t|
22
+ t.string :name, null: false
23
+ end
24
+ add_index :roles, :name, unique: true
25
+
26
+ create_table :permissions do |t|
27
+ t.string :name, null: false
28
+ end
29
+ add_index :permissions, :name, unique: true
30
+
31
+ create_table :acl_mappings do |t|
32
+ t.references :acl, null: false
33
+ t.references :role, null: false
34
+ t.references :permission, null: true
35
+
36
+ t.timestamps null: false
37
+ end
38
+
39
+ add_index :acl_mappings, [:acl_id, :role_id, :permission_id], unique: true
40
+
41
+ create_table :user_roles do |t|
42
+ t.references :user
43
+ t.references :role, null: false
44
+ t.references :resource, polymorphic: true
45
+
46
+ t.timestamps null: false
47
+ end
48
+ add_index :user_roles, [:resource_type, :resource_id]
49
+ # Only can't be admin of a resource multiple times
50
+ add_index :user_roles, [:user_id, :role_id, :resource_type, :resource_id],
51
+ unique: true, name: :user_roles_avoid_duplicate_roles
52
+
53
+ create_table :acl_ownerships do |t|
54
+ t.references :acl, null: false
55
+ t.references :resource, polymorphic: true
56
+ end
57
+ # There can be only one-to-many relationship with a resource
58
+ add_index :acl_ownerships, [:resource_type, :resource_id], unique: true
59
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe TestController, type: :controller do
4
+ before(:each) do
5
+ stub_const('Rails', double(root: double))
6
+ allow(Rails.root).to receive(:join)
7
+ .and_return('spec/support/acls/subject_acl.rb')
8
+ SkillTree::Parser::Initializer.parse!
9
+ end
10
+
11
+ let(:user) { User.create! }
12
+
13
+ let(:admin) do
14
+ user = User.create!
15
+ user.role! :admin, resource
16
+ user
17
+ end
18
+
19
+ let!(:resource) { Post.create! }
20
+
21
+ it '#index' do
22
+ expect_any_instance_of(TestController).not_to receive(:update)
23
+ expect { get :index }.to raise_error SkillTree::NotAuthorizedError
24
+ end
25
+
26
+ it '#show' do
27
+ expect_any_instance_of(TestController).not_to receive(:update)
28
+ expect { get :index }.to raise_error SkillTree::NotAuthorizedError
29
+ end
30
+
31
+ it '#update' do
32
+ expect_any_instance_of(TestController).to receive(:update)
33
+ get :update
34
+ end
35
+
36
+ describe '#create' do
37
+ it 'is open for a logged user' do
38
+ allow_any_instance_of(TestController).to receive(:current_user)
39
+ .and_return(user)
40
+ expect_any_instance_of(TestController).to receive(:create)
41
+ get :create
42
+ end
43
+
44
+ it 'is blocked for a guest' do
45
+ expect_any_instance_of(TestController).not_to receive(:create)
46
+ expect { get :create }.to raise_error SkillTree::NotAuthorizedError
47
+ end
48
+ end
49
+
50
+ describe '#destroy' do
51
+ it 'is open for an admin' do
52
+ allow_any_instance_of(TestController).to receive(:current_user)
53
+ .and_return(admin)
54
+ expect_any_instance_of(TestController).to receive(:destroy)
55
+ get :destroy
56
+ end
57
+
58
+ it 'is blocked for a logged user' do
59
+ allow_any_instance_of(TestController).to receive(:current_user)
60
+ .and_return(user)
61
+ expect_any_instance_of(TestController).not_to receive(:destroy)
62
+ expect { get :destroy }.to raise_error SkillTree::NotAuthorizedError
63
+ end
64
+
65
+ it 'is blocked for a guest' do
66
+ expect_any_instance_of(TestController).not_to receive(:destroy)
67
+ expect { get :destroy }.to raise_error SkillTree::NotAuthorizedError
68
+ end
69
+ end
70
+ end
71
+
72
+ describe Test2Controller, type: :controller do
73
+ before(:each) do
74
+ stub_const('Rails', double(root: double))
75
+ allow(Rails.root).to receive(:join)
76
+ .and_return('spec/support/acls/subject_acl.rb')
77
+ SkillTree::Parser::Initializer.parse!
78
+ end
79
+
80
+ let(:user) { User.create! }
81
+ let!(:resource) { Post.create! }
82
+
83
+ describe '#index' do
84
+ it 'returns error if the user is not logged in' do
85
+ expect_any_instance_of(Test2Controller).not_to receive(:index)
86
+ expect { get :index }.to raise_error SkillTree::NotAuthorizedError
87
+ end
88
+
89
+ it 'works only when user is logged in' do
90
+ allow_any_instance_of(Test2Controller).to receive(:current_user)
91
+ .and_return(user)
92
+
93
+ expect_any_instance_of(Test2Controller).to receive(:index)
94
+ get :index
95
+ end
96
+ end
97
+
98
+ describe '#current_acl' do
99
+ it 'differs between classes with common parent' do
100
+ expect(TestController.current_acl).not_to eq(Test2Controller.current_acl)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe SkillTree::InstallGenerator, type: :generator do
4
+ destination File.expand_path('../../tmp', __FILE__)
5
+ arguments %w()
6
+
7
+ before(:all) do
8
+ prepare_destination
9
+ run_generator
10
+ end
11
+
12
+ it 'creates a migration, an acl and an initializer' do
13
+ expect(destination_root).to have_structure {
14
+ no_file 'test.rb'
15
+ directory 'config' do
16
+ directory 'initializers' do
17
+ file 'skill_tree.rb' do
18
+ contains 'SkillTree.init!'
19
+ end
20
+ end
21
+ file 'acl.rb' do
22
+ contains 'Example acl.rb file'
23
+ end
24
+ end
25
+ directory 'db' do
26
+ directory 'migrate' do
27
+ migration 'create_skill_tree_acl_system' do
28
+ contains 'class CreateSkillTreeAclSystem'
29
+ end
30
+ end
31
+ end
32
+ }
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe SkillTree::Models::AclMapping, type: :model do
4
+ it 'belongs to an acl' do
5
+ expect(subject).to respond_to(:acl)
6
+ end
7
+
8
+ it 'validates presence of acl' do
9
+ expect(subject).to have(1).error_on(:acl)
10
+ subject.acl = build(:acl)
11
+ expect(subject).to have(0).error_on(:acl)
12
+ end
13
+
14
+ it 'belongs to a role' do
15
+ expect(subject).to respond_to(:role)
16
+ end
17
+
18
+ it 'validates presence of role' do
19
+ expect(subject).to have(1).error_on(:role)
20
+ subject.role = build(:role)
21
+ expect(subject).to have(0).error_on(:role)
22
+ end
23
+
24
+ it 'belongs to a permission' do
25
+ expect(subject).to respond_to(:permission)
26
+ end
27
+
28
+ it 'validates presence of permission' do
29
+ expect(subject).to have(1).error_on(:permission)
30
+ subject.permission = build(:permission)
31
+ expect(subject).to have(0).error_on(:permission)
32
+ end
33
+
34
+ it 'validates uniqueness of relations' do
35
+ acl_mapping = create(:acl_mapping,
36
+ role: create(:role, name: 'a'),
37
+ acl: create(:acl, name: 'b', version: 0),
38
+ permission: create(:permission, name: 'c'))
39
+ subject.assign_attributes(
40
+ role: acl_mapping.role,
41
+ acl: acl_mapping.acl,
42
+ permission: acl_mapping.permission
43
+ )
44
+ expect(subject).to have(1).error_on(:acl_id)
45
+ end
46
+ end