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 +4 -4
- data/README.md +84 -14
- data/app/modules/invitational/invitation_core.rb +26 -1
- data/app/modules/invitational/invited_to.rb +4 -0
- data/app/services/invitational/creates_system_user_invitation.rb +34 -0
- data/lib/generators/invitational/install/templates/invitation.rb +2 -0
- data/lib/invitational/cancan.rb +56 -14
- data/lib/invitational/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84e55fa3994b13b2b2d2916363decf894c4e8bff
|
4
|
+
data.tar.gz: bf6ee875a454487dbc6362cbb0a4cf8e59dab81c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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'
|
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
|
-
|
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
|
-
###
|
193
|
-
To
|
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,
|
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
|
-
|
270
|
+
Pass the following as the first attribute:
|
201
271
|
|
202
272
|
```
|
203
|
-
can :manage, Child, roles[
|
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,
|
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[
|
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
|
-
|
87
|
+
roles = Invitation.system_roles + [:uberadmin]
|
88
|
+
!roles.include?(role)
|
64
89
|
end
|
65
90
|
|
66
91
|
def role
|
@@ -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
|
data/lib/invitational/cancan.rb
CHANGED
@@ -22,7 +22,11 @@ module Invitational
|
|
22
22
|
role_type = subject
|
23
23
|
end
|
24
24
|
|
25
|
-
role_mappings
|
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.
|
41
|
+
in_roles.inject(false) do |result,role|
|
38
42
|
result || if role.respond_to? :values
|
39
|
-
|
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
|
-
|
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
|
data/lib/invitational/version.rb
CHANGED
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.
|
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-
|
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
|
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
|
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
|