invitational 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +215 -0
  4. data/Rakefile +38 -0
  5. data/app/assets/javascripts/invitational/application.js +15 -0
  6. data/app/assets/stylesheets/invitational/application.css +13 -0
  7. data/app/controllers/invitational/application_controller.rb +4 -0
  8. data/app/helpers/invitational/application_helper.rb +4 -0
  9. data/app/modules/invitational/accepts_invitation_as.rb +37 -0
  10. data/app/modules/invitational/invitation_core.rb +97 -0
  11. data/app/modules/invitational/invited_to.rb +29 -0
  12. data/app/services/invitational/checks_for_invitation.rb +35 -0
  13. data/app/services/invitational/claims_all_invitations.rb +14 -0
  14. data/app/services/invitational/claims_invitation.rb +22 -0
  15. data/app/services/invitational/creates_invitation.rb +32 -0
  16. data/app/services/invitational/creates_uber_admin_invitation.rb +33 -0
  17. data/app/views/layouts/invitational/application.html.erb +14 -0
  18. data/config/routes.rb +2 -0
  19. data/db/migrate/20130528220144_create_invitations.rb +19 -0
  20. data/lib/generators/invitational/install/USAGE +15 -0
  21. data/lib/generators/invitational/install/install_generator.rb +39 -0
  22. data/lib/generators/invitational/install/templates/ability.rb +19 -0
  23. data/lib/generators/invitational/install/templates/initializer.rb +3 -0
  24. data/lib/generators/invitational/install/templates/invitation.rb +7 -0
  25. data/lib/generators/invitational/make_invitable/make_invitable_generator.rb +22 -0
  26. data/lib/invitational.rb +2 -0
  27. data/lib/invitational/cancan.rb +63 -0
  28. data/lib/invitational/engine.rb +4 -0
  29. data/lib/invitational/exceptions.rb +9 -0
  30. data/lib/invitational/version.rb +3 -0
  31. data/lib/tasks/invitational_tasks.rake +12 -0
  32. 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,4 @@
1
+ module Invitational
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Invitational
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -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,2 @@
1
+ Invitational::Engine.routes.draw do
2
+ end
@@ -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,3 @@
1
+ Invitational.user_class = "<%= @identity_class %>"
2
+
3
+ Invitational.define_roles <%= @role_list %>
@@ -0,0 +1,7 @@
1
+ class Invitation < ActiveRecord::Base
2
+ include ActiveModel::ForbiddenAttributesProtection
3
+ include Invitational::InvitationCore
4
+
5
+ belongs_to :user<%= @custom_user_class %>
6
+
7
+ 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
@@ -0,0 +1,2 @@
1
+ require "invitational/engine"
2
+ require "invitational/exceptions"
@@ -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,4 @@
1
+ module Invitational
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ 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,3 @@
1
+ module Invitational
2
+ VERSION = "1.1.5"
3
+ 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: []