authorizable 0.9.0

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 (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