invitational 1.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +215 -0
- data/Rakefile +38 -0
- data/app/assets/javascripts/invitational/application.js +15 -0
- data/app/assets/stylesheets/invitational/application.css +13 -0
- data/app/controllers/invitational/application_controller.rb +4 -0
- data/app/helpers/invitational/application_helper.rb +4 -0
- data/app/modules/invitational/accepts_invitation_as.rb +37 -0
- data/app/modules/invitational/invitation_core.rb +97 -0
- data/app/modules/invitational/invited_to.rb +29 -0
- data/app/services/invitational/checks_for_invitation.rb +35 -0
- data/app/services/invitational/claims_all_invitations.rb +14 -0
- data/app/services/invitational/claims_invitation.rb +22 -0
- data/app/services/invitational/creates_invitation.rb +32 -0
- data/app/services/invitational/creates_uber_admin_invitation.rb +33 -0
- data/app/views/layouts/invitational/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20130528220144_create_invitations.rb +19 -0
- data/lib/generators/invitational/install/USAGE +15 -0
- data/lib/generators/invitational/install/install_generator.rb +39 -0
- data/lib/generators/invitational/install/templates/ability.rb +19 -0
- data/lib/generators/invitational/install/templates/initializer.rb +3 -0
- data/lib/generators/invitational/install/templates/invitation.rb +7 -0
- data/lib/generators/invitational/make_invitable/make_invitable_generator.rb +22 -0
- data/lib/invitational.rb +2 -0
- data/lib/invitational/cancan.rb +63 -0
- data/lib/invitational/engine.rb +4 -0
- data/lib/invitational/exceptions.rb +9 -0
- data/lib/invitational/version.rb +3 -0
- data/lib/tasks/invitational_tasks.rake +12 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 17a5a124a390f4503a9ee874522e80f511c03664
|
4
|
+
data.tar.gz: fc524576ad2faf67f5d8fa8d3901c0211a38d09d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67ebd517377d4256a47063e18f97739b94d74f9b73061b4c0a5a7abd41e0709b00da020fec4ca15c4bdcbd53b3150b6736c3daa20f8803a878f7f7414cd12b3a
|
7
|
+
data.tar.gz: 6a8a893f60048c60d765c08e1ddcc60b6cd2673d6e1cab718a0baa889cd5a5165fc254ba9343fe6dd97335b296cdcf30d6cd44b557a37e3f81b6ea43ad7f59e0
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
#Overview
|
2
|
+
|
3
|
+
The purpose of Invitational is to eliminate the tight coupling between user identity/authentication and application 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.
|
4
|
+
|
5
|
+
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.
|
6
|
+
|
7
|
+
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).
|
8
|
+
|
9
|
+
An invitation is initially created in an un-claimed state. The invitation is associated with an email address, but can be claimed by any user who has the unique claim hash. The Invitational library allows for this delegation of authority, though it is fully possible for a host application to implement a requirement that the user claiming an invitation must match the email for which the invitation was created. Once claimed, an invitation may not be claimed again by any other user.
|
10
|
+
|
11
|
+
|
12
|
+
#Getting Started
|
13
|
+
Invitational works with Rails 4.0 and up. You can add it to your Gemfile with:
|
14
|
+
|
15
|
+
```
|
16
|
+
gem 'invitational', git: 'git@github.com:d-i/invitational.git'
|
17
|
+
```
|
18
|
+
|
19
|
+
Run the bundle command to install it.
|
20
|
+
|
21
|
+
After you install the gem, you need to run the generator:
|
22
|
+
|
23
|
+
```
|
24
|
+
rails generate invitational:install MODEL
|
25
|
+
```
|
26
|
+
|
27
|
+
Replace MODEL with the class name of your identity class. Since this is very frequently `User`, the
|
28
|
+
generator defaults to that class name, thus you can omit it if that is how your application is built:
|
29
|
+
|
30
|
+
```
|
31
|
+
rails generate invitational:install
|
32
|
+
```
|
33
|
+
|
34
|
+
The generator will add a database migration you will need to run:
|
35
|
+
|
36
|
+
```
|
37
|
+
rake db:migrate
|
38
|
+
```
|
39
|
+
|
40
|
+
#Implementation
|
41
|
+
|
42
|
+
##invited_to
|
43
|
+
The generator will setup your identity model (`User`) to include the `Invitational::InvitedTo` module. As part of the Invitational
|
44
|
+
functionality it provides, the `invited_to` method is added to your user class along with the foundational has_many relationship to
|
45
|
+
Invitation. This method accepts a list of the entity classes (as symbols)
|
46
|
+
to which a user can be invited:
|
47
|
+
|
48
|
+
```
|
49
|
+
invited_to :customer, :vendor, :supplier
|
50
|
+
```
|
51
|
+
|
52
|
+
This will setup has_many :through relationships for each entity:
|
53
|
+
|
54
|
+
```
|
55
|
+
user.companies
|
56
|
+
user.vendors
|
57
|
+
user.suppliers
|
58
|
+
```
|
59
|
+
|
60
|
+
##accepts_invitation_for
|
61
|
+
To configure an entity as able to accept invitations, use the `make_invitable` generator:
|
62
|
+
|
63
|
+
```
|
64
|
+
rails generate invitational:make_invitable MODEL, ROLE1, ROLE2...
|
65
|
+
```
|
66
|
+
|
67
|
+
Here, replace MODEL with the name of the entity class you are making invitable. Replace, ROLE1, ROLE2 with the
|
68
|
+
list of roles which are valid to this model, for example User, Admin. The generator will include the `Invitational::AcceptsInvitationAs`
|
69
|
+
module, and will pre-populate the call to the `accepts_invitation_as` method with the list of roles supplied:
|
70
|
+
|
71
|
+
```
|
72
|
+
accepts_invitation_as :user, :admin
|
73
|
+
```
|
74
|
+
|
75
|
+
As with your identity class, a foundational has_many relationship is established with Invitation. The `accepts_invitation_as`
|
76
|
+
method also sets up has_many :through relationships to user for each role identified:
|
77
|
+
|
78
|
+
```
|
79
|
+
entity.users
|
80
|
+
entity.admins
|
81
|
+
```
|
82
|
+
|
83
|
+
You can then add this entity to the list of invitable classes on the `invited_to` call in your identity class.
|
84
|
+
|
85
|
+
#Usage
|
86
|
+
##Creating Invitations
|
87
|
+
To create an invitation to a given model:
|
88
|
+
|
89
|
+
```
|
90
|
+
entity = Entity.find(1)
|
91
|
+
|
92
|
+
entity.invite "foo@bar.com", :admin
|
93
|
+
```
|
94
|
+
|
95
|
+
The method will return the Invitation. In the event that the email has already been invited to that entity,
|
96
|
+
an `Invitational::AlreadyInvitedError` will be raised. If the passed role is not valid for the given entity (based on its
|
97
|
+
`accepts_invitation_as` call), an `Invitational::InvalidRoleError` will be raised.
|
98
|
+
|
99
|
+
###Immediately Claimed Invitations
|
100
|
+
|
101
|
+
In some situations it is preferable to have an invitation created that is immediately claimed by an existing user.
|
102
|
+
For example, if the current user is creating an invitable entity, they would likely want to have immediate administrative
|
103
|
+
authority to that entity. In such situations, you can pass a user object (an instance of your identity class) to
|
104
|
+
the invite method instead of an email. The invitation that is created will be immedately claimed by that user:
|
105
|
+
|
106
|
+
```
|
107
|
+
entity = Entity.create(...)
|
108
|
+
|
109
|
+
entity.invite current_user, :admin
|
110
|
+
```
|
111
|
+
|
112
|
+
##Claiming Invitations
|
113
|
+
|
114
|
+
Invitations can be claimed by passing their hash and the claiming user to the `claim` class method on Invitation:
|
115
|
+
|
116
|
+
```
|
117
|
+
Invitation.claim claim_hash, current_user
|
118
|
+
```
|
119
|
+
|
120
|
+
The method will return the claimed Invitation. In the event that the hash does match an existing invitation,
|
121
|
+
an `Invitational::InvitationNotFoundError` will be raised. If the hash is found, but the invitation has already
|
122
|
+
been claimed, an `Invitational::AlreadyClaimedError` will be raised.
|
123
|
+
|
124
|
+
##Checking for Invitations
|
125
|
+
|
126
|
+
The `invited_to?` instance method that Invitational adds to your identity class provides an easy interface to
|
127
|
+
check if a user has an accepted invitation to a specific entity. Your query can be general (invited in any role) or
|
128
|
+
specifically for a supplied role:
|
129
|
+
|
130
|
+
```
|
131
|
+
current_user.invited_to? entity
|
132
|
+
```
|
133
|
+
|
134
|
+
Will return true if the current user has accepted an invitation in any role to the entity.
|
135
|
+
|
136
|
+
```
|
137
|
+
current_user.invited_to? entity, :admin
|
138
|
+
```
|
139
|
+
|
140
|
+
Will only return true if the current user has accepted an invitation as an Admin to the entity.
|
141
|
+
|
142
|
+
##UberAdmin
|
143
|
+
|
144
|
+
Invitational provides a special, system-wide, invitation and role called `:uberadmin`. A user that has
|
145
|
+
claimed an UberAdmin invitation will always indicate they have been invited to a given role for a given entity.
|
146
|
+
In other words, every call to `invited_to?` for an UberAdmin will return true.
|
147
|
+
|
148
|
+
To create an UberAdmin invitation:
|
149
|
+
|
150
|
+
```
|
151
|
+
Invitation.invite_uberadmin "foo@bar.com"
|
152
|
+
```
|
153
|
+
|
154
|
+
As with creating standard invitations, you can pass a user instead of an email to have the invitation
|
155
|
+
claimed immediately by that user:
|
156
|
+
|
157
|
+
```
|
158
|
+
Invitation.invite_uberadmin current_user
|
159
|
+
```
|
160
|
+
|
161
|
+
The process to claim an UberAdmin invitation is the same as any other invitation.
|
162
|
+
|
163
|
+
To make getting started with a brand new Invitational based environment easier, a rake task is provided to
|
164
|
+
create a new UberAdmin invitation.
|
165
|
+
|
166
|
+
```
|
167
|
+
rake invitational:create_uberadmin
|
168
|
+
```
|
169
|
+
|
170
|
+
This will output the claim hash for a new UberAdmin invitation.
|
171
|
+
|
172
|
+
You can test to see if the a user is an uberadmin through:
|
173
|
+
|
174
|
+
```
|
175
|
+
current_user.uberadmin?
|
176
|
+
```
|
177
|
+
|
178
|
+
##CanCan
|
179
|
+
|
180
|
+
Invitational adds a new condition key to CanCan's abilities, `:role`. This allows you to define the role(s)
|
181
|
+
that a user must be invited into for a specific entity in order to perform the specified action. For example,
|
182
|
+
to indicate that a user invited to a parent entity in an admin role can manage the parent entity, but a user
|
183
|
+
invited to a staff role can only read the parent entity, in your `ability.rb` file:
|
184
|
+
|
185
|
+
```
|
186
|
+
can :manage, Parent, roles: [:admin]
|
187
|
+
can :read, Parent, roles: [:staff]
|
188
|
+
```
|
189
|
+
|
190
|
+
###Invitation to a parent
|
191
|
+
To idenfitify abilities based upon invitations to a parent entity, add a hash as an element to the roles array,
|
192
|
+
supplying the parent attribute name as a key, and an allowed roles array as the value:
|
193
|
+
|
194
|
+
```
|
195
|
+
can :manage, Child, roles[ {parent: [:admin, :staff]}]
|
196
|
+
```
|
197
|
+
|
198
|
+
The parent invitation can be used recursively too, to specify grand-parent (or above) relationships:
|
199
|
+
|
200
|
+
```
|
201
|
+
can :manage, Child, roles[ {parent: {grand_parent: [:admin, :staff]}}]
|
202
|
+
```
|
203
|
+
|
204
|
+
To specify child and parent invitations, you can combine them on one line:
|
205
|
+
|
206
|
+
```
|
207
|
+
can :manage, Child, roles[:child_admin, {parent: [:admin, :staff]}]
|
208
|
+
```
|
209
|
+
|
210
|
+
However, it is recommended to specify them on separate lines:
|
211
|
+
|
212
|
+
```
|
213
|
+
can :manage, Child, roles[:child_admin]
|
214
|
+
can :manage, Child, roles[ {parent: [:admin, :staff]}]
|
215
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
require "bundler/gem_tasks"
|
16
|
+
|
17
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'Invitational'
|
20
|
+
rdoc.options << '--line-numbers'
|
21
|
+
rdoc.rdoc_files.include('README.rdoc')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// the compiled file.
|
9
|
+
//
|
10
|
+
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
11
|
+
// GO AFTER THE REQUIRES BELOW.
|
12
|
+
//
|
13
|
+
//= require jquery
|
14
|
+
//= require jquery_ujs
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Invitational
|
2
|
+
module AcceptsInvitationAs
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :invitations, :as => :invitable, dependent: :destroy
|
7
|
+
|
8
|
+
@roles = Array.new
|
9
|
+
|
10
|
+
def self.roles
|
11
|
+
@roles
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def accepts_invitation_as *args
|
18
|
+
args.each do |role|
|
19
|
+
relation = role.to_s.pluralize.to_sym
|
20
|
+
|
21
|
+
has_many relation, -> {where "invitations.role = '#{role.to_s}'"}, through: :invitations, source: :user
|
22
|
+
|
23
|
+
self.roles << role
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def invite target, role
|
29
|
+
unless self.class.roles.include? role
|
30
|
+
raise Invitational::InvalidRoleError.new
|
31
|
+
end
|
32
|
+
|
33
|
+
Invitational::CreatesInvitation.for self, target, role
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Invitational
|
2
|
+
module InvitationCore
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
belongs_to :invitable, :polymorphic => true
|
7
|
+
|
8
|
+
before_create :setup_hash
|
9
|
+
|
10
|
+
validates :email, :presence => true
|
11
|
+
validates :role, :presence => true
|
12
|
+
validates :invitable, :presence => true, :if => :standard_role?
|
13
|
+
|
14
|
+
scope :uberadmin, lambda {
|
15
|
+
where("invitable_id IS NULL AND role = 'uberadmin'")
|
16
|
+
}
|
17
|
+
|
18
|
+
scope :for_email, lambda {|email|
|
19
|
+
where('email = ?', email)
|
20
|
+
}
|
21
|
+
|
22
|
+
scope :pending_for, lambda {|email|
|
23
|
+
where('email = ? AND user_id IS NULL', email)
|
24
|
+
}
|
25
|
+
|
26
|
+
scope :for_claim_hash, lambda {|claim_hash|
|
27
|
+
where('claim_hash = ?', claim_hash)
|
28
|
+
}
|
29
|
+
|
30
|
+
scope :for_invitable, lambda {|type, id|
|
31
|
+
where('invitable_type = ? AND invitable_id = ?', type, id)
|
32
|
+
}
|
33
|
+
|
34
|
+
scope :by_role, lambda {|role|
|
35
|
+
where('role = ?', role.to_s)
|
36
|
+
}
|
37
|
+
|
38
|
+
scope :pending, lambda { where('user_id IS NULL') }
|
39
|
+
scope :claimed, lambda { where('user_id IS NOT NULL') }
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def claim claim_hash, user
|
44
|
+
Invitational::ClaimsInvitation.for claim_hash, user
|
45
|
+
end
|
46
|
+
|
47
|
+
def claim_all_for user
|
48
|
+
Invitational::ClaimsAllInvitations.for user
|
49
|
+
end
|
50
|
+
|
51
|
+
def invite_uberadmin target
|
52
|
+
Invitational::CreatesUberAdminInvitation.for target
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_hash
|
58
|
+
self.date_sent = DateTime.now
|
59
|
+
self.claim_hash = Digest::SHA1.hexdigest(email + date_sent.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
def standard_role?
|
63
|
+
role != :uberadmin
|
64
|
+
end
|
65
|
+
|
66
|
+
def role
|
67
|
+
unless super.nil?
|
68
|
+
super.to_sym
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def role=(value)
|
73
|
+
super(value.to_sym)
|
74
|
+
role
|
75
|
+
end
|
76
|
+
|
77
|
+
def role_title
|
78
|
+
if uberadmin?
|
79
|
+
"Uber Admin"
|
80
|
+
else
|
81
|
+
role.to_s.titleize
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def uberadmin?
|
86
|
+
invitable.nil? == true && role == :uberadmin
|
87
|
+
end
|
88
|
+
|
89
|
+
def claimed?
|
90
|
+
date_accepted.nil? == false
|
91
|
+
end
|
92
|
+
|
93
|
+
def unclaimed?
|
94
|
+
!claimed?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Invitational
|
2
|
+
module InvitedTo
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :invitations, dependent: :destroy
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def invited_to *args
|
11
|
+
args.each do |entity|
|
12
|
+
relation = entity.to_s.pluralize.to_sym
|
13
|
+
type = entity.to_s.camelize
|
14
|
+
|
15
|
+
has_many relation, through: :invitations, source: :invitable, source_type: type
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def uberadmin?
|
21
|
+
invitations.uberadmin.count > 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def invited_to? entity, role=nil
|
25
|
+
Invitational::ChecksForInvitation.for self, entity,role
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Invitational
|
2
|
+
class ChecksForInvitation
|
3
|
+
|
4
|
+
def self.for user, invitable, roles=nil
|
5
|
+
self.uberadmin?(user) || self.specific_invite?(user, invitable, roles)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def self.uberadmin? user
|
11
|
+
user.invitations.uberadmin.count == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.specific_invite? user, invitable, roles
|
15
|
+
invites = user.invitations.for_invitable(invitable.class.name, invitable.id)
|
16
|
+
|
17
|
+
if invites.count > 0
|
18
|
+
unless roles.nil?
|
19
|
+
self.role_check invites.first, roles
|
20
|
+
else
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.role_check invitation, roles
|
27
|
+
if roles.respond_to? :map
|
28
|
+
roles.include? invitation.role
|
29
|
+
else
|
30
|
+
invitation.role == roles
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Invitational
|
2
|
+
class ClaimsAllInvitations
|
3
|
+
|
4
|
+
def self.for user
|
5
|
+
pending_invitations = ::Invitation.pending_for(user.email)
|
6
|
+
|
7
|
+
pending_invitations.each do |invitation|
|
8
|
+
invitation.user = user
|
9
|
+
invitation.date_accepted = DateTime.now
|
10
|
+
invitation.save
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Invitational
|
2
|
+
class ClaimsInvitation
|
3
|
+
|
4
|
+
def self.for claim_hash, user
|
5
|
+
invitation = Invitation.for_claim_hash(claim_hash).first
|
6
|
+
|
7
|
+
if invitation.nil?
|
8
|
+
raise Invitational::InvitationNotFoundError.new
|
9
|
+
end
|
10
|
+
|
11
|
+
if invitation.claimed?
|
12
|
+
raise Invitational::AlreadyClaimedError.new
|
13
|
+
end
|
14
|
+
|
15
|
+
invitation.user = user
|
16
|
+
invitation.date_accepted = DateTime.now
|
17
|
+
invitation.save
|
18
|
+
|
19
|
+
invitation
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Invitational
|
2
|
+
class CreatesInvitation
|
3
|
+
|
4
|
+
def self.for invitable, target, role
|
5
|
+
if target.is_a? String
|
6
|
+
email = target
|
7
|
+
|
8
|
+
if invitable.invitations.for_email(email).count > 0
|
9
|
+
raise Invitational::AlreadyInvitedError
|
10
|
+
end
|
11
|
+
|
12
|
+
else
|
13
|
+
user = target
|
14
|
+
email = user.email
|
15
|
+
|
16
|
+
if user.invitations.for_invitable(invitable.class, invitable.id).count > 0
|
17
|
+
raise Invitational::AlreadyInvitedError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
invitation = ::Invitation.new(invitable: invitable, role: role, email: email)
|
22
|
+
if user
|
23
|
+
invitation.user = user
|
24
|
+
invitation.date_accepted = DateTime.now
|
25
|
+
end
|
26
|
+
invitation.save
|
27
|
+
|
28
|
+
invitation
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Invitational
|
2
|
+
class CreatesUberAdminInvitation
|
3
|
+
attr_reader :success,
|
4
|
+
:invitation
|
5
|
+
|
6
|
+
def self.for target
|
7
|
+
if target.is_a? String
|
8
|
+
email = target
|
9
|
+
|
10
|
+
if Invitation.uberadmin.for_email(email).count > 0
|
11
|
+
raise Invitational::AlreadyInvitedError.new
|
12
|
+
end
|
13
|
+
else
|
14
|
+
user = target
|
15
|
+
email = user.email
|
16
|
+
|
17
|
+
if user.uberadmin?
|
18
|
+
raise Invitational::AlreadyInvitedError.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
invitation = ::Invitation.new(role: :uberadmin, email: email)
|
23
|
+
if user
|
24
|
+
invitation.user = user
|
25
|
+
invitation.date_accepted = DateTime.now
|
26
|
+
end
|
27
|
+
invitation.save
|
28
|
+
|
29
|
+
invitation
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Invitational</title>
|
5
|
+
<%= stylesheet_link_tag "invitational/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "invitational/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateInvitations < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :invitations do |t|
|
4
|
+
t.string :email
|
5
|
+
t.string :role
|
6
|
+
t.references :invitable, polymorphic: true
|
7
|
+
t.integer :user_id
|
8
|
+
t.datetime :date_sent
|
9
|
+
t.datetime :date_accepted
|
10
|
+
t.string :claim_hash
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :invitations, :invitable_id
|
16
|
+
add_index :invitations, :invitable_type
|
17
|
+
add_index :invitations, :user_id
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Description:
|
2
|
+
This generator installs the necessary components for the Invitational gem, and provides the initial relatoinship
|
3
|
+
to your identiy class
|
4
|
+
|
5
|
+
Example:
|
6
|
+
rails generate invitational User
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
app/model/ability.rb
|
10
|
+
app/model/invitation.rb
|
11
|
+
db/migrate/XXXX_create_invitations.invitational_engine.rb
|
12
|
+
|
13
|
+
This will update:
|
14
|
+
app/model/user.rb
|
15
|
+
Gemfile
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Invitational
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
argument :identity_class, type: :string, default: "User", banner: "Class name of identity model (e.g. User)"
|
7
|
+
|
8
|
+
def invitation_model
|
9
|
+
@identity_class = identity_class.gsub(/\,/,"").camelize
|
10
|
+
|
11
|
+
if @identity_class != "User"
|
12
|
+
@custom_user_class = ", :class_name => '#{@identity_class}'"
|
13
|
+
else
|
14
|
+
@custom_user_class = ""
|
15
|
+
end
|
16
|
+
|
17
|
+
template "invitation.rb", "app/models/invitation.rb"
|
18
|
+
end
|
19
|
+
|
20
|
+
def ability_model
|
21
|
+
@identity_model = @identity_class.underscore
|
22
|
+
template "ability.rb", "app/models/ability.rb"
|
23
|
+
end
|
24
|
+
|
25
|
+
def link_to_identity_model
|
26
|
+
path = "app/models/#{@identity_model}.rb"
|
27
|
+
content = " include Invitational::InvitedTo\n"
|
28
|
+
|
29
|
+
inject_into_class path, @identity_class.constantize, content
|
30
|
+
end
|
31
|
+
|
32
|
+
def install_migration
|
33
|
+
rake("invitational_engine:install:migrations")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'invitational/cancan'
|
2
|
+
|
3
|
+
class Ability
|
4
|
+
include CanCan::Ability
|
5
|
+
include Invitational::CanCan::Ability
|
6
|
+
|
7
|
+
attr_reader :role_mappings, :<%= @identity_model %>
|
8
|
+
|
9
|
+
def initialize(<%= @identity_model %>)
|
10
|
+
|
11
|
+
@role_mappings = {}
|
12
|
+
@<%= @identity_model %> = <%= @identity_model %>
|
13
|
+
|
14
|
+
# Example:
|
15
|
+
# can :manage, Entity, roles: [:admin]
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Invitational
|
2
|
+
module Generators
|
3
|
+
class MakeInvitableGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
argument :entity_class, type: :string, banner: "Class name of entity class to which users will be invited"
|
7
|
+
argument :roles, type: :array, default: ["role"], banner: "List of Valid Roles"
|
8
|
+
|
9
|
+
def add_invitational_reference
|
10
|
+
@entity_class = entity_class.gsub(/\,/,"").camelize
|
11
|
+
@entity_model = @entity_class.underscore
|
12
|
+
@path = "app/models/#{@entity_model}.rb"
|
13
|
+
@role_list = roles.map{|role| ":" + role.gsub(/\,/,"")}.join(", ")
|
14
|
+
|
15
|
+
content = " include Invitational::AcceptsInvitationAs\n accepts_invitation_as #{@role_list}\n"
|
16
|
+
|
17
|
+
inject_into_class @path, @entity_class.constantize, content
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/invitational.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Invitational
|
2
|
+
module CanCan
|
3
|
+
module Ability
|
4
|
+
|
5
|
+
def can(action = nil, subject = nil, conditions = nil, &block)
|
6
|
+
if conditions && conditions.has_key?(:roles)
|
7
|
+
roles = conditions.delete(:roles) if conditions
|
8
|
+
conditions = nil if conditions and conditions.empty?
|
9
|
+
|
10
|
+
block ||= setup_role_based_block_for roles, subject, action
|
11
|
+
end
|
12
|
+
|
13
|
+
rules << ::CanCan::Rule.new(true, action, subject, conditions, block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_role_based_block_for roles, subject, action
|
17
|
+
key = subject.name.underscore + action.to_s
|
18
|
+
|
19
|
+
if roles.respond_to? :values
|
20
|
+
role_type, roles = [roles.keys.first, roles.values.first]
|
21
|
+
else
|
22
|
+
role_type = subject
|
23
|
+
end
|
24
|
+
|
25
|
+
role_mappings[key] = roles
|
26
|
+
|
27
|
+
block = ->(model){
|
28
|
+
roles = role_mappings[key]
|
29
|
+
check_permission_for model, user, roles
|
30
|
+
}
|
31
|
+
|
32
|
+
block
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_permission_for model, user, in_roles
|
36
|
+
|
37
|
+
in_roles.reduce(false) do |result,role|
|
38
|
+
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
|
+
|
50
|
+
else
|
51
|
+
Invitational::ChecksForInvitation.for(user, model, role)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def attribute_roles attribute, roles
|
58
|
+
{attribute => roles}
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Invitational
|
2
|
+
class InvitationalError < StandardError; end
|
3
|
+
|
4
|
+
class InvalidRoleError < InvitationalError; end
|
5
|
+
class AlreadyInvitedError < InvitationalError; end
|
6
|
+
|
7
|
+
class InvitationNotFoundError < InvitationalError; end
|
8
|
+
class AlreadyClaimedError < InvitationalError; end
|
9
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :invitational do
|
2
|
+
desc "Creates an Uberadmin User"
|
3
|
+
task :create_uberadmin => [:environment] do
|
4
|
+
email = Digest::SHA1.hexdigest(DateTime.now.to_s) + "@localhost"
|
5
|
+
|
6
|
+
creator = Invitational::CreatesUberAdminInvitation.for email
|
7
|
+
invitation = creator.invitation
|
8
|
+
|
9
|
+
puts "Your uberadmin invitation claim hash is: #{invitation.claim_hash}"
|
10
|
+
puts "Visit your claim URL with this hash to claim the invitation."
|
11
|
+
end
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: invitational
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dave Goerlich
|
8
|
+
- Joe Fiorini
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-03-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 4.0.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 4.0.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: cancancan
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: sqlite3
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: capybara
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.0.2
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.0.2
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: combustion
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 2.12.0
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 2.12.0
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: rspec-rails
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 2.12.0
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 2.12.0
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rspec-given
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 2.3.0
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 2.3.0
|
126
|
+
description: See summary
|
127
|
+
email:
|
128
|
+
- dave@d-i.co
|
129
|
+
- joe@d-i.co
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- MIT-LICENSE
|
135
|
+
- README.md
|
136
|
+
- Rakefile
|
137
|
+
- app/assets/javascripts/invitational/application.js
|
138
|
+
- app/assets/stylesheets/invitational/application.css
|
139
|
+
- app/controllers/invitational/application_controller.rb
|
140
|
+
- app/helpers/invitational/application_helper.rb
|
141
|
+
- app/modules/invitational/accepts_invitation_as.rb
|
142
|
+
- app/modules/invitational/invitation_core.rb
|
143
|
+
- app/modules/invitational/invited_to.rb
|
144
|
+
- app/services/invitational/checks_for_invitation.rb
|
145
|
+
- app/services/invitational/claims_all_invitations.rb
|
146
|
+
- app/services/invitational/claims_invitation.rb
|
147
|
+
- app/services/invitational/creates_invitation.rb
|
148
|
+
- app/services/invitational/creates_uber_admin_invitation.rb
|
149
|
+
- app/views/layouts/invitational/application.html.erb
|
150
|
+
- config/routes.rb
|
151
|
+
- db/migrate/20130528220144_create_invitations.rb
|
152
|
+
- lib/generators/invitational/install/USAGE
|
153
|
+
- lib/generators/invitational/install/install_generator.rb
|
154
|
+
- lib/generators/invitational/install/templates/ability.rb
|
155
|
+
- lib/generators/invitational/install/templates/initializer.rb
|
156
|
+
- lib/generators/invitational/install/templates/invitation.rb
|
157
|
+
- lib/generators/invitational/make_invitable/make_invitable_generator.rb
|
158
|
+
- lib/invitational.rb
|
159
|
+
- lib/invitational/cancan.rb
|
160
|
+
- lib/invitational/engine.rb
|
161
|
+
- lib/invitational/exceptions.rb
|
162
|
+
- lib/invitational/version.rb
|
163
|
+
- lib/tasks/invitational_tasks.rake
|
164
|
+
homepage: http://github.com/d-i/invitational
|
165
|
+
licenses: []
|
166
|
+
metadata: {}
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 2.2.0
|
184
|
+
signing_key:
|
185
|
+
specification_version: 4
|
186
|
+
summary: Manage users and the objects they belong to
|
187
|
+
test_files: []
|