authorizable 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +33 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +178 -0
  6. data/LICENSE +22 -0
  7. data/README.md +80 -0
  8. data/Rakefile +1 -0
  9. data/authorizable.gemspec +40 -0
  10. data/config/locales/en.yml +6 -0
  11. data/lib/authorizable.rb +31 -0
  12. data/lib/authorizable/controller.rb +156 -0
  13. data/lib/authorizable/model.rb +162 -0
  14. data/lib/authorizable/permission_utilities.rb +89 -0
  15. data/lib/authorizable/permissions.rb +112 -0
  16. data/lib/authorizable/version.rb +5 -0
  17. data/spec/integration/controller_spec.rb +127 -0
  18. data/spec/integration/model_spec.rb +169 -0
  19. data/spec/rails_helper.rb +14 -0
  20. data/spec/spec_helper.rb +48 -0
  21. data/spec/support/definitions.rb +16 -0
  22. data/spec/support/factories.rb +17 -0
  23. data/spec/support/factory_girl.rb +7 -0
  24. data/spec/support/rails_app/Rakefile +6 -0
  25. data/spec/support/rails_app/app/assets/images/.keep +0 -0
  26. data/spec/support/rails_app/app/assets/javascripts/application.js +16 -0
  27. data/spec/support/rails_app/app/assets/javascripts/some_resources.js +2 -0
  28. data/spec/support/rails_app/app/assets/javascripts/users.js +2 -0
  29. data/spec/support/rails_app/app/assets/stylesheets/application.css +15 -0
  30. data/spec/support/rails_app/app/assets/stylesheets/scaffold.css +56 -0
  31. data/spec/support/rails_app/app/assets/stylesheets/some_resources.css +4 -0
  32. data/spec/support/rails_app/app/assets/stylesheets/users.css +4 -0
  33. data/spec/support/rails_app/app/controllers/application_controller.rb +14 -0
  34. data/spec/support/rails_app/app/controllers/events_controller.rb +58 -0
  35. data/spec/support/rails_app/app/controllers/users_controller.rb +58 -0
  36. data/spec/support/rails_app/app/helpers/application_helper.rb +2 -0
  37. data/spec/support/rails_app/app/helpers/events_helper.rb +2 -0
  38. data/spec/support/rails_app/app/helpers/users_helper.rb +2 -0
  39. data/spec/support/rails_app/app/mailers/.keep +0 -0
  40. data/spec/support/rails_app/app/models/collaboration.rb +16 -0
  41. data/spec/support/rails_app/app/models/concerns/.keep +0 -0
  42. data/spec/support/rails_app/app/models/discount.rb +3 -0
  43. data/spec/support/rails_app/app/models/event.rb +5 -0
  44. data/spec/support/rails_app/app/models/user.rb +9 -0
  45. data/spec/support/rails_app/app/views/events/_form.html.erb +17 -0
  46. data/spec/support/rails_app/app/views/events/edit.html.erb +6 -0
  47. data/spec/support/rails_app/app/views/events/index.html.erb +25 -0
  48. data/spec/support/rails_app/app/views/events/new.html.erb +5 -0
  49. data/spec/support/rails_app/app/views/events/show.html.erb +4 -0
  50. data/spec/support/rails_app/app/views/layouts/application.html.erb +14 -0
  51. data/spec/support/rails_app/app/views/users/_form.html.erb +17 -0
  52. data/spec/support/rails_app/app/views/users/edit.html.erb +6 -0
  53. data/spec/support/rails_app/app/views/users/index.html.erb +25 -0
  54. data/spec/support/rails_app/app/views/users/new.html.erb +5 -0
  55. data/spec/support/rails_app/app/views/users/show.html.erb +4 -0
  56. data/spec/support/rails_app/bin/bundle +3 -0
  57. data/spec/support/rails_app/bin/rails +8 -0
  58. data/spec/support/rails_app/bin/rake +4 -0
  59. data/spec/support/rails_app/config.ru +0 -0
  60. data/spec/support/rails_app/config/application.rb +29 -0
  61. data/spec/support/rails_app/config/boot.rb +3 -0
  62. data/spec/support/rails_app/config/database.yml +25 -0
  63. data/spec/support/rails_app/config/environment.rb +5 -0
  64. data/spec/support/rails_app/config/environments/development.rb +41 -0
  65. data/spec/support/rails_app/config/environments/production.rb +79 -0
  66. data/spec/support/rails_app/config/environments/test.rb +42 -0
  67. data/spec/support/rails_app/config/initializers/assets.rb +11 -0
  68. data/spec/support/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  69. data/spec/support/rails_app/config/initializers/cookies_serializer.rb +3 -0
  70. data/spec/support/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
  71. data/spec/support/rails_app/config/initializers/inflections.rb +16 -0
  72. data/spec/support/rails_app/config/initializers/mime_types.rb +4 -0
  73. data/spec/support/rails_app/config/initializers/session_store.rb +3 -0
  74. data/spec/support/rails_app/config/initializers/wrap_parameters.rb +14 -0
  75. data/spec/support/rails_app/config/locales/en.yml +23 -0
  76. data/spec/support/rails_app/config/routes.rb +60 -0
  77. data/spec/support/rails_app/config/secrets.yml +22 -0
  78. data/spec/support/rails_app/db/development.sqlite3 +0 -0
  79. data/spec/support/rails_app/db/migrate/20141231134904_create_users.rb +8 -0
  80. data/spec/support/rails_app/db/migrate/20150102221633_create_collaborations.rb +13 -0
  81. data/spec/support/rails_app/db/migrate/20150102225507_create_events.rb +9 -0
  82. data/spec/support/rails_app/db/migrate/20150104171110_create_discounts.rb +11 -0
  83. data/spec/support/rails_app/db/schema.rb +45 -0
  84. data/spec/support/rails_app/db/seeds.rb +7 -0
  85. data/spec/support/rails_app/db/test.sqlite3 +0 -0
  86. data/spec/support/rails_app/log/development.log +26296 -0
  87. data/spec/support/rails_app/public/404.html +67 -0
  88. data/spec/support/rails_app/public/422.html +67 -0
  89. data/spec/support/rails_app/public/500.html +66 -0
  90. data/spec/support/rails_app/public/favicon.ico +0 -0
  91. data/spec/support/rails_app/public/robots.txt +5 -0
  92. data/spec/unit/permission_utilities_spec.rb +157 -0
  93. data/spec/unit/permissions_spec.rb +65 -0
  94. metadata +352 -0
@@ -0,0 +1,162 @@
1
+ module Authorizable
2
+ # this should be included on the 'User' model or whatever
3
+ # is going to be performing action that need to be
4
+ # authorized
5
+ module Model
6
+ extend ActiveSupport::Concern
7
+
8
+ # @TODO figure out how to make roles generic
9
+ IS_OWNER = 0
10
+ IS_UNRELATED = 1
11
+
12
+ def method_missing(name, *args, &block)
13
+ string_name = name.to_s
14
+
15
+ if string_name =~ /can_(.+)\?/
16
+ permission_name = $1
17
+ permission_name.gsub!('destroy', 'delete')
18
+ if ["delete", "edit", "create"].include?(permission_name)
19
+ # shorthand for delete_{object_name}
20
+ object = args[0]
21
+ object_name = object.class.name.downcase
22
+ return send("can_#{permission_name}_#{object_name}?", *args, &block)
23
+ else
24
+ return process_permission(name, permission_name, args)
25
+ end
26
+ else
27
+ super(name, *args, &block)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def process_permission(method_name, permission_name, args)
34
+ permission = permission_name.to_sym
35
+ o = args[0] # object; Event, Discount, etc
36
+ # default to allow
37
+ result = true
38
+
39
+ role = get_role_of(o)
40
+
41
+ # don't perform the permission evaluation, if we have already computed it
42
+ permission_value_from_cache = value_from_permission_cache(method_name, role)
43
+ return permission_value_from_cache if permission_value_from_cache.present?
44
+
45
+
46
+ # evaluate procs
47
+ if (proc = PermissionUtilities.has_procs?(permission))
48
+ result &= proc.call(o, self)
49
+ end
50
+
51
+ # Here is where the addition of adding collaborations may reside
52
+
53
+ # finally, determine if the user (self) can do the requested action
54
+ result &= can?(permission, role)
55
+
56
+ # so we don't need to do everything again
57
+ set_permission_cache(
58
+ name: method_name,
59
+ value: result,
60
+ role: role
61
+ )
62
+
63
+ result
64
+ end
65
+
66
+ # @param [String] permission name of the permission
67
+ # @param [Number] role role of the user
68
+ # @param [Hash] set a hash of string keys and values
69
+ # @return [Boolean] the result of the whether or not the user, self,
70
+ # is allowed to perform the action
71
+ def can?(permission, role = IS_OWNER, set = {})
72
+ result = true
73
+ use_default = false
74
+
75
+ use_default = true if set[permission].nil?
76
+
77
+ if use_default
78
+ result &= PermissionUtilities.value_for(permission, role)
79
+ else
80
+ result &= set[permission]
81
+ end
82
+
83
+ result
84
+ end
85
+
86
+ # By default this will just be a pass-through to has_role_with
87
+ # This method is intended to be a part of a group/role implementation.
88
+ # for example, if working with a hierarchy of objects, such as a
89
+ # Book having many chapters, and the chapters themselves don't have a User
90
+ # but the Book does, that logic should be added in a method that overrides this one.
91
+ #
92
+ # @param [ActiveRecord::Base] object should be the object that is being tested
93
+ # if the user can perform the action on
94
+ def get_role_of(object)
95
+ return has_role_with(object)
96
+ end
97
+
98
+ # This method can also be overridden if one desires to have multiple types of
99
+ # ownership, such as a collaborator-type relationship
100
+ #
101
+ # @param [ActiveRecord::Base] object should be the object that is being tested
102
+ # if the user can perform the action on
103
+ # @return [Number] true if self owns object
104
+ def has_role_with(object)
105
+ if object.respond_to?(:user_id)
106
+ if object.user_id == self.id
107
+ return IS_OWNER
108
+ else
109
+ return IS_UNRELATED
110
+ end
111
+ end
112
+ # hopefully the object passed always responds to user_id
113
+ IS_UNRELATED
114
+ end
115
+
116
+ # calculating the value of a permission is costly.
117
+ # there are several Database lookups and lots of merging
118
+ # of hashes.
119
+ # once a permission is calculated, we'll store it here, so we don't
120
+ # have to re-calculate/query/merge everything all over again
121
+ #
122
+ # for both object access and page access, check if we've
123
+ # already calculated the permission
124
+ #
125
+ # the structure of this cache is the following:
126
+ # {
127
+ # role_1: {
128
+ # permission1: true
129
+ # permission2: false
130
+ # },
131
+ # authorization_permission_name: true
132
+ # }
133
+ #
134
+ # @param [String] name name of the permission
135
+ # @param [Number] role role of the user
136
+ # @param [Boolean] value
137
+ def set_permission_cache(name: "", role: nil, value: nil)
138
+ @permission_cache ||= {}
139
+ if role
140
+ @permission_cache[role] ||= {}
141
+ @permission_cache[role][name] = value
142
+ else
143
+ @permission_cache[name] = value
144
+ end
145
+ end
146
+
147
+ # @param [String] permission_name name of the permission
148
+ # @param [Number] role role of the user
149
+ # @return [Boolean] value of the previously stored permission
150
+ def value_from_permission_cache(permission_name, role = nil)
151
+ @permission_cache ||= {}
152
+
153
+ if role
154
+ @permission_cache[role] ||= {}
155
+ @permission_cache[role][permission_name]
156
+ else
157
+ @permission_cache[permission_name]
158
+ end
159
+ end
160
+
161
+ end
162
+ end
@@ -0,0 +1,89 @@
1
+ module Authorizable
2
+
3
+ module PermissionUtilities
4
+
5
+ KIND = 0
6
+ DEFAULT_ACCESS = 1
7
+ DESCRIPTION = 2
8
+ VISIBILITY_PROC = 3
9
+ ACCESS_PROC = 4
10
+
11
+ OBJECT = 0
12
+ ACCESS = 1
13
+ DEFAULT_ROLE = 0
14
+
15
+ def self.permissions
16
+ Authorizable::Permissions.definitions
17
+ end
18
+
19
+ def self.set_for_role(role)
20
+ permissions.inject({}) { |h,(k, v)|
21
+ value = v[DEFAULT_ACCESS]
22
+ h[k.to_sym] = value.is_a?(Array) ? value[role] : value
23
+ h
24
+ }
25
+ end
26
+
27
+ # returns procs or false
28
+ def self.has_procs?(permission)
29
+ permission_data_helper(permission, ACCESS_PROC)
30
+ end
31
+
32
+ def self.has_visibility_procs?(permission)
33
+ permission_data_helper(permission, VISIBILITY_PROC)
34
+ end
35
+
36
+ def self.should_render?(permission, *args)
37
+ result = true
38
+ proc = self.has_visibility_procs?(permission)
39
+
40
+ if proc
41
+ result = proc.call(*args)
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ def self.description_for(permission)
48
+ result = permission_data_helper(permission.to_sym, DESCRIPTION)
49
+
50
+ if result.blank?
51
+ result = permission.to_s.humanize
52
+ end
53
+
54
+ result
55
+ end
56
+
57
+ def self.value_for(permission, role = DEFAULT_ROLE)
58
+ value = permissions[permission.to_sym][DEFAULT_ACCESS]
59
+ value.is_a?(Array) ? value[role] : value
60
+ end
61
+
62
+ def self.has_key?(permission)
63
+ permissions[permission.to_sym].present?
64
+ end
65
+
66
+ def self.is_access?(permission)
67
+ permissions[permission][KIND] == ACCESS
68
+ end
69
+
70
+ def self.is_object?(permission)
71
+ permissions[permission][KIND] == OBJECT
72
+ end
73
+
74
+ private
75
+
76
+ def self.permission_data_helper(permission, position)
77
+ result = false
78
+ data = permissions[permission]
79
+
80
+ if data && data.length >= (position + 1)
81
+ result = data[position]
82
+ end
83
+
84
+ result
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,112 @@
1
+ module Authorizable
2
+
3
+ class Permissions
4
+
5
+ # Aliased constants for easier typing / readability
6
+ OBJECT = PermissionUtilities::OBJECT
7
+ ACCESS = PermissionUtilities::ACCESS
8
+
9
+ # defaults for a resource
10
+ CRUD_TYPES = {
11
+ edit: OBJECT,
12
+ delete: OBJECT,
13
+ create: OBJECT,
14
+ view: ACCESS
15
+ }
16
+
17
+ # where all the permission definitions are stored
18
+ #
19
+ # example structure:
20
+ # {
21
+ # edit_organization: [OBJECT, [true, false]],
22
+ # delete_organization: [OBJECT, [true, false], nil, ->(e, user){ e.hosted_by == user }, ->(e, user){ e.owner == user }],
23
+ # create_organization: [ACCESS, [true, false], nil, nil],
24
+ #
25
+ # edit_collaborator: [OBJECT, [true, false]],
26
+ # delete_collaborator: [OBJECT, [true, false]],
27
+ # create_collaborator: [OBJECT, [true, false]],
28
+ # view_collaborators: [OBJECT, [true, false]],
29
+ #
30
+ # view_attendees: [OBJECT, true],
31
+ # view_unpaid_attendees: [OBJECT, true],
32
+ # view_cancelled_registrations: [OBJECT, true]
33
+ # }
34
+ #
35
+ # note that because this is a hash, order and organization of like-named
36
+ # permissions is non-existent
37
+ class_attribute :definitions
38
+
39
+ # @example:
40
+ # {
41
+ # update_event: [OBJECT, true, "Edit Event"],
42
+ # delete_event: [OBJECT, [true, false, false], nil, ->(e, user){ e.hosted_by == user }],
43
+ # create_event: [ACCESS, RESTRICT_COLLABORATORS]
44
+ # }
45
+ # CRUD authorizations can be expcitly defined
46
+ #
47
+ # @example
48
+ # {
49
+ # crud: [
50
+ # object_name: [true, false, false],
51
+ # ojbect2_name: true,
52
+ # ]
53
+ # }
54
+ # by providing a :crud array in the hash will generate permissions
55
+ # for the specified object: create, delete, read, and update
56
+ #
57
+ # @note:
58
+ # update is aliased with edit, and may be used interchangeably
59
+ # delete is aliased with destroy, and may be used interchangeably
60
+ #
61
+ # @note:
62
+ # descriptions are not provided by default, and are only specifiable
63
+ # when explicitly defining permissions (not using crud)
64
+ #
65
+ # @param [Hash] permissions
66
+ def self.set(permissions)
67
+ cruds = permissions.delete(:crud)
68
+
69
+ self.definitions = permissions
70
+
71
+ if cruds.present?
72
+ cruds.each do |set|
73
+ set.each do |key, values_for_roles|
74
+ CRUD_TYPES.each do |action, kind|
75
+ permission = "#{action}_#{key}"
76
+ permission << "s" if kind == ACCESS # need a better way to pluralize
77
+ permission = permission.to_sym
78
+ permission_array = [kind, values_for_roles]
79
+ self.definitions[permission] = permission_array
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # similar to how CanCan does the creation of permission
87
+ # but without the need for a user to exist immediately
88
+ #
89
+ # @param [Symbol] name what the permission should be called
90
+ # (the can prefix is automatic, and should be excluded)
91
+ # @param [Boolean] allow (true) default authorization for this permission
92
+ # @param [Array] allow (true) default authorization for this permission
93
+ # @param [String] description (nil) how to explain this permission
94
+ # @param [Proc] visibility (nil) conditions used when rendering this permission in the UI
95
+ # @param [Proc] conditions (nil) additional conditions used when authorizing a user
96
+ # @param [Number] kind (OBJECT) used to specify if this permission takes access on an object or not
97
+ def self.can(name, allow = true, description = nil, visibility = nil, conditions = nil, kind = OBJECT)
98
+ permission_array = [kind, allow, description, visibility, conditions]
99
+ self.add(name, permission_array)
100
+ end
101
+
102
+ private
103
+
104
+ # @param [Symbol] key permission name
105
+ # @param [Array] array settings for permission
106
+ def self.add(key, array)
107
+ self.definitions[key.to_sym] = array
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,5 @@
1
+ module Authorizable
2
+
3
+ VERSION = "0.9.0"
4
+
5
+ end
@@ -0,0 +1,127 @@
1
+ require 'rails_helper'
2
+
3
+ describe EventsController, type: :controller do
4
+
5
+ it 'includes authorizable' do
6
+ expect(controller.class.ancestors).to include(Authorizable::Controller)
7
+ end
8
+
9
+ it 'calls authorizable' do
10
+ allow_any_instance_of(EventsController).to receive(:authorizable)
11
+ get :index
12
+ end
13
+
14
+ context 'actions' do
15
+ before(:each) do
16
+ EventsController.send(
17
+ :authorizable,
18
+ edit: {
19
+ target: :event,
20
+ redirect_path: Proc.new{ event_path(@event) }
21
+ },
22
+ create: {
23
+ permission: :can_create_event?,
24
+ redirect_path: Proc.new{ events_path }
25
+ },
26
+ destroy: {
27
+ target: :event,
28
+ redirect_path: Proc.new{ event_path(@event) }
29
+ }
30
+ )
31
+ end
32
+
33
+ context 'is authorized' do
34
+ let(:user){ create(:user) }
35
+ let(:event){ create(:event, user: user) }
36
+
37
+ it 'is allowed' do
38
+ expect(controller).to receive(:authorizable_authorized?)
39
+ get :edit, id: event.id
40
+ expect(assigns(:event)).to eq event
41
+ end
42
+ end
43
+
44
+ context 'redirects on' do
45
+
46
+ context 'json requests' do
47
+ let(:event){ create(:event) }
48
+ let(:user){ create(:user) }
49
+
50
+ after(:each) do
51
+ expect(response.status).to eq 401
52
+ end
53
+
54
+ it 'edit' do
55
+ allow(user).to receive(:can_edit?){ false }
56
+ allow(controller).to receive(:current_user){ user }
57
+
58
+ get :edit, id: event, format: :json
59
+ end
60
+ end
61
+
62
+ context 'html requests' do
63
+ let(:event){ create(:event) }
64
+ let(:user){ create(:user) }
65
+
66
+ after(:each) do
67
+ expect(flash[:alert]).to eq I18n.t('authorizable.not_authorized')
68
+ end
69
+
70
+ context 'edit' do
71
+ before(:each) do
72
+ allow(user).to receive(:can_edit?){ false }
73
+ allow(controller).to receive(:current_user){ user }
74
+ end
75
+
76
+ it 'to show' do
77
+ get :edit, id: event.id
78
+ expected = { action: :show, id: event.id }
79
+ expect(response).to redirect_to expected
80
+ end
81
+
82
+ context 'and update' do
83
+ it 'to show' do
84
+ put :update, id: event.id
85
+ expected = { action: :show, id: event.id }
86
+ expect(response).to redirect_to expected
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ context 'create' do
93
+ before(:each) do
94
+ allow(user).to receive(:can_create_event?){ false }
95
+ allow(controller).to receive(:current_user){ user }
96
+ end
97
+
98
+ it 'to index' do
99
+ post :create
100
+ expect(response).to redirect_to action: :index
101
+ end
102
+
103
+ context 'and new' do
104
+ it 'to index' do
105
+ get :new
106
+ expect(response).to redirect_to action: :index
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ context 'destroy' do
113
+ before(:each) do
114
+ allow(user).to receive(:can_destroy?).and_return(false)
115
+ allow(controller).to receive(:current_user){ user }
116
+ end
117
+
118
+ it 'to show' do
119
+ delete :destroy, id: event.id
120
+ expect(response).to redirect_to action: :show
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ end