invitational 1.1.6 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e6be26e6c2ba8c14eedc3c84029c42fb2d8ba27c
4
- data.tar.gz: adfd381d2d9b1484211b2dd56e153eda3f806990
3
+ metadata.gz: 84e55fa3994b13b2b2d2916363decf894c4e8bff
4
+ data.tar.gz: bf6ee875a454487dbc6362cbb0a4cf8e59dab81c
5
5
  SHA512:
6
- metadata.gz: 2b893c783473e121a31c412639316039b8e8aec19b508b7a660949f0ccefea5e245939d0d7872c11aefa9d322a55c22d14a68560624347e6a323db3f126ccd23
7
- data.tar.gz: fa83149b377ba8788bee298f644346e8f1363def5b3c63ccb9f8bf3d2ce9bdf0709c9996c748c7ef05864e0e1cba4a18dc1606716fc8507ca2d0afacfea7b78e
6
+ metadata.gz: 055d06625f07896c2da05dec3b70ed14b00901f18b6af5822e5c893b628cec1835e52c5eb9093ff924dcddd804d2e2063cd3935e1f05ac275edd80fb9108cfac
7
+ data.tar.gz: a961c3c3a27703b6dd8314dc1144baffc4320fccbb2aeb62750f31406ad7cbe523414890fd7a7658d63d5876eb1fc1b85e6f82e8746ccaf039af1ab1627bc14c
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  The purpose of Invitational is to eliminate the tight coupling between user identity/authentication and application functional authorization. It is a common pattern in multi-user systems that in order to grant access to someone else, an existing administrator must create a user account, providing a username and password, and then grant permissions to that account. The administrator then needs to communicate the username and password to the individual, often via email. The complexity of this process is compounded in multi-account based systems where a single user might wind up with mutiple user accounts with various usernames and passwords.
6
6
 
7
- Inspired by 37Signals' single sign-on process for Basecamp, Invitational provides an intermediate layer between an identity model (i.e. User) and some entity to which authorization is given. This intermediate layer, an Invitation, represents a granted role for a given entity. These roles can then be leveraged by the application's functional authorization system.
7
+ Inspired by 37Signals' single sign-on process for Basecamp, Invitational provides an intermediate layer between an identity model (i.e. User) and either the system as a whole or some specific entity to which authorization is given. This intermediate layer, an Invitation, represents a granted role for the sytem or a given entity. These roles can then be leveraged by the application's functional authorization system.
8
8
 
9
9
  Invitational supplies a custom DSL on top of the CanCan gem to provide an easy implementation of role-based functional authorization. This DSL supports the hierarchical model common in many systems. Permissions can be esablished for a child based upon an invitation to its parent (or grandparent, etc).
10
10
 
@@ -15,7 +15,7 @@ An invitation is initially created in an un-claimed state. The invitation is as
15
15
  Invitational works with Rails 4.0 and up. You can add it to your Gemfile with:
16
16
 
17
17
  ```
18
- gem 'invitational', git: 'git@github.com:d-i/invitational.git'
18
+ gem 'invitational'
19
19
  ```
20
20
 
21
21
  Run the bundle command to install it.
@@ -39,6 +39,21 @@ The generator will add a database migration you will need to run:
39
39
  rake db:migrate
40
40
  ```
41
41
 
42
+ #Types of Invitations
43
+ Invitational has three types of invitations:
44
+
45
+ ##Entity
46
+ An `Entity` invitation, as the name imples, is for a specific entity within the system. For example, in a contract management system, a user might be invited to a
47
+ contract in the sytem with the role of 'Recipient' . They might then be able to read and to mark that specific contract as signed, but not access any other contracts in the system.
48
+
49
+ ##System
50
+ A `System` invitation is not related to a specific entity, but to the system overall. For example, in the contract management system mentioned above, another user might be
51
+ invited to the sytem with the role of 'contract_manager'. They might then be able to manage *all* contracts within the system, but not have authority to invite other users.
52
+
53
+ ##UberAdmin
54
+ An `UberAdmin` invitation is also, like a `System` invitation, not related to a specific entity but to the system overall. Unlike a `System` invitation, an `UberAdmin` invitation
55
+ effectively grants the associated user access to all parts of the system, as every inquiry for the existance of an invitation (either System or Entity) will indicate true.
56
+
42
57
  #Implementation
43
58
 
44
59
  ##invited_to
@@ -84,9 +99,24 @@ entity.admins
84
99
 
85
100
  You can then add this entity to the list of invitable classes on the `invited_to` call in your identity class.
86
101
 
102
+ ##accepts_system_roles_as
103
+ System roles are defined in the `Invitation` class. Simply add the list of system roles to the class method that has been defined for you by the
104
+ generator:
105
+
106
+ ```
107
+ accepts_system_roles_as :contract_manager, :bookkeeper
108
+ ```
109
+
110
+ The `accepts_system_roles_as` method also sets up scopes on `Invitation` for each identified role:
111
+
112
+ ```
113
+ License.contract_managers
114
+ License.bookkeepers
115
+ ```
116
+
87
117
  #Usage
88
118
  ##Creating Invitations
89
- To create an invitation to a given model:
119
+ To create an entity invitation to a given model:
90
120
 
91
121
  ```
92
122
  entity = Entity.find(1)
@@ -94,9 +124,16 @@ entity = Entity.find(1)
94
124
  entity.invite "foo@bar.com", :admin
95
125
  ```
96
126
 
97
- The method will return the Invitation. In the event that the email has already been invited to that entity,
127
+ To create an invitation to a system role:
128
+
129
+ ```
130
+ Invitation.invite_system_user "foo@bar.com", :contract_manager
131
+ ```
132
+
133
+ The method will return the Invitation. In the event that the email has already been invited to that entity or to the system role,
98
134
  an `Invitational::AlreadyInvitedError` will be raised. If the passed role is not valid for the given entity (based on its
99
- `accepts_invitation_as` call), an `Invitational::InvalidRoleError` will be raised.
135
+ `accepts_invitation_as` call) or not a valid system role, an `Invitational::InvalidRoleError` will be raised.
136
+
100
137
 
101
138
  ###Immediately Claimed Invitations
102
139
 
@@ -111,6 +148,13 @@ entity = Entity.create(...)
111
148
  entity.invite current_user, :admin
112
149
  ```
113
150
 
151
+ and
152
+
153
+ ```
154
+ Invitation.invite_system_user current_user, :contract_manager
155
+ ```
156
+
157
+
114
158
  ##Claiming Invitations
115
159
 
116
160
  Invitations can be claimed by passing their hash and the claiming user to the `claim` class method on Invitation:
@@ -141,11 +185,17 @@ current_user.invited_to? entity, :admin
141
185
 
142
186
  Will only return true if the current user has accepted an invitation as an Admin to the entity.
143
187
 
188
+ For system roles, the `invited_to_system?` instance method on your identity class can be used:
189
+
190
+ ```
191
+ current_user.invited_to_system? :contract_manager
192
+ ```
193
+
144
194
  ##UberAdmin
145
195
 
146
196
  Invitational provides a special, system-wide, invitation and role called `:uberadmin`. A user that has
147
197
  claimed an UberAdmin invitation will always indicate they have been invited to a given role for a given entity.
148
- In other words, every call to `invited_to?` for an UberAdmin will return true.
198
+ In other words, every call to `invited_to?` or `invited_to_system?` for an UberAdmin will return true.
149
199
 
150
200
  To create an UberAdmin invitation:
151
201
 
@@ -189,29 +239,49 @@ can :manage, Parent, roles: [:admin]
189
239
  can :read, Parent, roles: [:staff]
190
240
  ```
191
241
 
192
- ###Invitation to a parent
193
- To idenfitify abilities based upon invitations to a parent entity, add a hash as an element to the roles array,
194
- supplying the parent attribute name as a key, and an allowed roles array as the value:
242
+ ###System Roles
243
+ To specify system roles for a given ability, utilize the `system_roles` method inside a `roles:` array:
195
244
 
196
245
  ```
197
- can :manage, Child, roles[ {parent: [:admin, :staff]}]
246
+ can :manage, contract, roles: [system_roles(:contract_manager, :sales_manager)]
247
+ ```
248
+
249
+
250
+ ###Invitation to a parent (or other attribute)
251
+ To idenfitify abilities based upon invitations to a parent entity or other attribute, Invitational provides an
252
+ ```attribute_roles``` method. The first argument is symbol indicating the attribute name of the parent entity,
253
+ the second is an array of roles in which the user must be invited to the parent entity:
254
+
255
+ ```
256
+ can :manage, Child, roles[attribute_roles(:parent, [:admin, :staff])]
257
+ ```
258
+
259
+ To reference invitations on a "grand parent" (or higher) entity, ```attribute_roles``` optionally accepts
260
+ an array as the first parameter, indicating the "path" to the target entity.
261
+
262
+ To indicate that an invitation to the grandparent found here:
263
+
264
+ ```
265
+ entity = Entity.first
266
+
267
+ entity.parent.grandparent
198
268
  ```
199
269
 
200
- The parent invitation can be used recursively too, to specify grand-parent (or above) relationships:
270
+ Pass the following as the first attribute:
201
271
 
202
272
  ```
203
- can :manage, Child, roles[ {parent: {grand_parent: [:admin, :staff]}}]
273
+ can :manage, Child, roles[attribute_roles([:parent, :grandparent], [:admin])]
204
274
  ```
205
275
 
206
276
  To specify child and parent invitations, you can combine them on one line:
207
277
 
208
278
  ```
209
- can :manage, Child, roles[:child_admin, {parent: [:admin, :staff]}]
279
+ can :manage, Child, roles[:child_admin, attribute_roles(:parent, [:admin, :staff])]
210
280
  ```
211
281
 
212
282
  However, it is recommended to specify them on separate lines:
213
283
 
214
284
  ```
215
285
  can :manage, Child, roles[:child_admin]
216
- can :manage, Child, roles[ {parent: [:admin, :staff]}]
286
+ can :manage, Child, roles[attribute_roles(:parent, [:admin, :staff])]
217
287
  ```
@@ -35,8 +35,18 @@ module Invitational
35
35
  where('role = ?', role.to_s)
36
36
  }
37
37
 
38
+ scope :for_system_role, lambda {|role|
39
+ where('invitable_id IS NULL AND role = ?', role.to_s)
40
+ }
41
+
38
42
  scope :pending, lambda { where('user_id IS NULL') }
39
43
  scope :claimed, lambda { where('user_id IS NOT NULL') }
44
+
45
+ @system_roles = Array.new
46
+
47
+ def self.system_roles
48
+ @system_roles
49
+ end
40
50
  end
41
51
 
42
52
  module ClassMethods
@@ -52,6 +62,20 @@ module Invitational
52
62
  Invitational::CreatesUberAdminInvitation.for target
53
63
  end
54
64
 
65
+ def invite_system_user target, role
66
+ Invitational::CreatesSystemUserInvitation.for target, role
67
+ end
68
+
69
+ def accepts_system_roles_as *args
70
+ args.each do |role|
71
+ relation = role.to_s.pluralize.to_sym
72
+
73
+ scope relation, -> {where("invitable_id IS NULL AND role = '#{role.to_s}'")}
74
+
75
+ self.system_roles << role
76
+ end
77
+ end
78
+
55
79
  end
56
80
 
57
81
  def setup_hash
@@ -60,7 +84,8 @@ module Invitational
60
84
  end
61
85
 
62
86
  def standard_role?
63
- role != :uberadmin
87
+ roles = Invitation.system_roles + [:uberadmin]
88
+ !roles.include?(role)
64
89
  end
65
90
 
66
91
  def role
@@ -25,5 +25,9 @@ module Invitational
25
25
  Invitational::ChecksForInvitation.for self, entity,role
26
26
  end
27
27
 
28
+ def invited_to_system? role
29
+ invitations.for_system_role(role).count > 0
30
+ end
31
+
28
32
  end
29
33
  end
@@ -0,0 +1,34 @@
1
+ module Invitational
2
+ class CreatesSystemUserInvitation
3
+ attr_reader :success,
4
+ :invitation
5
+
6
+ def self.for target, role
7
+ if target.is_a? String
8
+ email = target
9
+
10
+ if Invitation.for_system_role(role).for_email(email).count > 0
11
+ raise Invitational::AlreadyInvitedError.new
12
+ end
13
+
14
+ else
15
+ user = target
16
+ email = user.email
17
+
18
+ if user.invited_to_system? role
19
+ raise Invitational::AlreadyInvitedError.new
20
+ end
21
+ end
22
+
23
+ invitation = ::Invitation.new(role: role, email: email)
24
+ if user
25
+ invitation.user = user
26
+ invitation.date_accepted = DateTime.now
27
+ end
28
+ invitation.save
29
+
30
+ invitation
31
+ end
32
+
33
+ end
34
+ end
@@ -4,4 +4,6 @@ class Invitation < ActiveRecord::Base
4
4
 
5
5
  belongs_to :user<%= @custom_user_class %>
6
6
 
7
+ accepts_system_roles_as
8
+
7
9
  end
@@ -22,7 +22,11 @@ module Invitational
22
22
  role_type = subject
23
23
  end
24
24
 
25
- role_mappings[key] = roles
25
+ unless role_mappings.has_key?(key)
26
+ role_mappings[key] = []
27
+ end
28
+
29
+ role_mappings[key] += roles
26
30
 
27
31
  block = ->(model){
28
32
  roles = role_mappings[key]
@@ -34,19 +38,9 @@ module Invitational
34
38
 
35
39
  def check_permission_for model, user, in_roles
36
40
 
37
- in_roles.reduce(false) do |result,role|
41
+ in_roles.inject(false) do |result,role|
38
42
  result || if role.respond_to? :values
39
- method = role.keys.first
40
- related = model.send(method)
41
-
42
- if related.respond_to? :any?
43
- related.any? do |model|
44
- check_permission_for model, user, role.values.flatten
45
- end
46
- else
47
- check_permission_for related, user, role.values.flatten
48
- end
49
-
43
+ check_permission_for_keyed_roles model, user, role
50
44
  else
51
45
  Invitational::ChecksForInvitation.for(user, model, role)
52
46
  end
@@ -54,8 +48,56 @@ module Invitational
54
48
 
55
49
  end
56
50
 
51
+ def check_permission_for_keyed_roles model, user, role
52
+ key = role.keys.first
53
+
54
+ if key == :system_roles
55
+ check_permission_for_system_role user, role
56
+ else
57
+ check_permission_for_attribute model, user, role
58
+ end
59
+ end
60
+
61
+ def check_permission_for_system_role user, role
62
+ roles = role.values.flatten
63
+
64
+ user.uberadmin? || roles.any? do |system_role|
65
+ user.invited_to_system? system_role
66
+ end
67
+ end
68
+
69
+ def check_permission_for_attribute model, user, role
70
+ method = role.keys.first
71
+ related = model.send(method)
72
+
73
+ if related.respond_to? :any?
74
+ related.any? do |model|
75
+ check_permission_for model, user, role.values.flatten
76
+ end
77
+ else
78
+ check_permission_for related, user, role.values.flatten
79
+ end
80
+ end
81
+
57
82
  def attribute_roles attribute, roles
58
- {attribute => roles}
83
+ hash = nil
84
+ if attribute.respond_to? :each
85
+ attribute.reverse.each do |attr|
86
+ if hash.nil?
87
+ hash = {attr => roles}
88
+ else
89
+ hash = {attr => hash}
90
+ end
91
+ end
92
+ else
93
+ hash = {attribute => roles}
94
+ end
95
+
96
+ hash
97
+ end
98
+
99
+ def system_roles roles
100
+ {system_roles: roles}
59
101
  end
60
102
 
61
103
  end
@@ -1,3 +1,3 @@
1
1
  module Invitational
2
- VERSION = "1.1.6"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: invitational
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dave Goerlich
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-03-07 00:00:00.000000000 Z
12
+ date: 2014-05-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: 4.0.0
20
+ version: '4.0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: 4.0.0
27
+ version: '4.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: cancancan
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -146,6 +146,7 @@ files:
146
146
  - app/services/invitational/claims_all_invitations.rb
147
147
  - app/services/invitational/claims_invitation.rb
148
148
  - app/services/invitational/creates_invitation.rb
149
+ - app/services/invitational/creates_system_user_invitation.rb
149
150
  - app/services/invitational/creates_uber_admin_invitation.rb
150
151
  - app/views/layouts/invitational/application.html.erb
151
152
  - config/routes.rb