restful_authentication 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. data/CHANGELOG +68 -0
  2. data/LICENSE +20 -0
  3. data/README.textile +232 -0
  4. data/Rakefile +54 -0
  5. data/TODO +15 -0
  6. data/generators/authenticated/USAGE +1 -0
  7. data/generators/authenticated/authenticated_generator.rb +493 -0
  8. data/generators/authenticated/lib/insert_routes.rb +69 -0
  9. data/generators/authenticated/templates/_model_partial.html.erb +8 -0
  10. data/generators/authenticated/templates/activation.erb +3 -0
  11. data/generators/authenticated/templates/authenticated_system.rb +189 -0
  12. data/generators/authenticated/templates/authenticated_test_helper.rb +12 -0
  13. data/generators/authenticated/templates/controller.rb +43 -0
  14. data/generators/authenticated/templates/features/accounts.feature +67 -0
  15. data/generators/authenticated/templates/features/sessions.feature +77 -0
  16. data/generators/authenticated/templates/features/step_definitions/ra_env.rb +7 -0
  17. data/generators/authenticated/templates/features/step_definitions/user_steps.rb +31 -0
  18. data/generators/authenticated/templates/helper.rb +2 -0
  19. data/generators/authenticated/templates/login.html.erb +14 -0
  20. data/generators/authenticated/templates/machinist_spec.rb +5 -0
  21. data/generators/authenticated/templates/machinist_test.rb +5 -0
  22. data/generators/authenticated/templates/mailer.rb +25 -0
  23. data/generators/authenticated/templates/migration.rb +24 -0
  24. data/generators/authenticated/templates/model.rb +83 -0
  25. data/generators/authenticated/templates/model_controller.rb +96 -0
  26. data/generators/authenticated/templates/model_helper.rb +93 -0
  27. data/generators/authenticated/templates/model_helper_spec.rb +157 -0
  28. data/generators/authenticated/templates/observer.rb +11 -0
  29. data/generators/authenticated/templates/signup.html.erb +19 -0
  30. data/generators/authenticated/templates/signup_notification.erb +8 -0
  31. data/generators/authenticated/templates/site_keys.rb +38 -0
  32. data/generators/authenticated/templates/spec/blueprints/user.rb +13 -0
  33. data/generators/authenticated/templates/spec/controllers/access_control_spec.rb +89 -0
  34. data/generators/authenticated/templates/spec/controllers/authenticated_system_spec.rb +107 -0
  35. data/generators/authenticated/templates/spec/controllers/sessions_controller_spec.rb +138 -0
  36. data/generators/authenticated/templates/spec/controllers/users_controller_spec.rb +197 -0
  37. data/generators/authenticated/templates/spec/fixtures/users.yml +60 -0
  38. data/generators/authenticated/templates/spec/helpers/users_helper_spec.rb +141 -0
  39. data/generators/authenticated/templates/spec/models/user_spec.rb +298 -0
  40. data/generators/authenticated/templates/tasks/auth.rake +33 -0
  41. data/generators/authenticated/templates/test/functional_test.rb +84 -0
  42. data/generators/authenticated/templates/test/mailer_test.rb +31 -0
  43. data/generators/authenticated/templates/test/model_functional_test.rb +91 -0
  44. data/generators/authenticated/templates/test/unit_test.rb +177 -0
  45. data/lib/authentication.rb +40 -0
  46. data/lib/authentication/by_cookie_token.rb +82 -0
  47. data/lib/authentication/by_password.rb +64 -0
  48. data/lib/authorization.rb +14 -0
  49. data/lib/authorization/aasm_roles.rb +64 -0
  50. data/lib/authorization/stateful_roles.rb +64 -0
  51. data/lib/restful_authentication.rb +6 -0
  52. data/lib/trustification.rb +14 -0
  53. data/lib/trustification/email_validation.rb +20 -0
  54. data/notes/AccessControl.txt +2 -0
  55. data/notes/Authentication.txt +5 -0
  56. data/notes/Authorization.txt +154 -0
  57. data/notes/RailsPlugins.txt +78 -0
  58. data/notes/SecurityFramework.graffle +0 -0
  59. data/notes/SecurityFramework.png +0 -0
  60. data/notes/SecurityPatterns.txt +163 -0
  61. data/notes/Tradeoffs.txt +126 -0
  62. data/notes/Trustification.txt +49 -0
  63. data/restful_authentication.gemspec +32 -0
  64. metadata +128 -0
@@ -0,0 +1,68 @@
1
+ h1. Internal Changes to code
2
+
3
+ As always, this is just a copy-and-pasted version of the CHANGELOG file in the source code tree.
4
+
5
+ h2. Changes for the May, 2008 version of restful-authentication
6
+
7
+ h3. Changes to user model
8
+
9
+ * recently_activated? belongs only if stateful
10
+ * Gave migration a 40-char limit on remember_token & an index on users by login
11
+ * **Much** stricter login and email validation
12
+ * put length constraints in migration too
13
+ * password in 6, 40
14
+ * salt and remember_token now much less predictability
15
+
16
+ h3. Changes to session_controller
17
+
18
+ * use uniform logout function
19
+ * use uniform remember_cookie functions
20
+ * avoid calling logged_in? which will auto-log-you-in (safe in the face of
21
+ logout! call, but idiot-proof)
22
+ * Moved reset_session into only the "now logged in" branch
23
+ ** wherever it goes, it has to be in front of the current_user= call
24
+ ** See more in README-Tradeoffs.txt
25
+ * made a place to take action on failed login attempt
26
+ * recycle login and remember_me setting on failed login
27
+ * nil'ed out the password field in 'new' view
28
+
29
+ h3. Changes to users_controller
30
+
31
+ * use uniform logout function
32
+ * use uniform remember_cookie functions
33
+ * Moved reset_session into only the "now logged in" branch
34
+ ** wherever it goes, it has to be in front of the current_user= call
35
+ ** See more in README-Tradeoffs.txt
36
+ * made the implicit login only happen for non-activationed sites
37
+ * On a failed signup, kick you back to the signin screen (but strip out the password & confirmation)
38
+ * more descriptive error messages in activate()
39
+
40
+ h3. users_helper
41
+
42
+ * link_to_user, link_to_current_user, link_to_signin_with_IP
43
+ * if_authorized(action, resource, &block) view function (with appropriate
44
+ warning)
45
+
46
+ h3. authenticated_system
47
+
48
+ * Made authorized? take optional arguments action=nil, resource=nil, *args
49
+ This makes its signature better match traditional approaches to access control
50
+ eg Reference Monitor in "Security Patterns":http://www.securitypatterns.org/patterns.html)
51
+ * authorized? should be a helper too
52
+ * added uniform logout! methods
53
+ * format.any (as found in access_denied) doesn't work until
54
+ http://dev.rubyonrails.org/changeset/8987 lands.
55
+ * cookies are now refreshed each time we cross the logged out/in barrier, as
56
+ "best":http://palisade.plynt.com/issues/2004Jul/safe-auth-practices/
57
+ "practice":http://www.owasp.org/index.php/Session_Management#Regeneration_of_Session_Tokens
58
+
59
+ h3. Other
60
+
61
+ * Used escapes <%= %> in email templates (among other reasons, so courtenay's
62
+ "'dumbass' test":http://tinyurl.com/684g9t doesn't complain)
63
+ * Added site key to generator, users.yml.
64
+ * Made site key generation idempotent in the most crude and hackish way
65
+ * 100% coverage apart from the stateful code. (needed some access_control
66
+ checks, and the http_auth stuff)
67
+ * Stories!
68
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 rick olson
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.
@@ -0,0 +1,232 @@
1
+ h1. "Restful Authentication Generator":http://github.com/technoweenie/restful-authentication
2
+
3
+ This widely-used plugin provides a foundation for securely managing user
4
+ authentication:
5
+ * Login / logout
6
+ * Secure password handling
7
+ * Account activation by validating email
8
+ * Account approval / disabling by admin
9
+ * Rudimentary hooks for authorization and access control.
10
+
11
+ This fork significantly updates the plugin to provide:
12
+ * 100% passing specs
13
+ * 100% passing cucumber features (with selenium support)
14
+ * Machinist support
15
+ * Pickle support
16
+ * Available as a gem
17
+ * Uses aasm gem
18
+
19
+ Several features were updated in May, 2008.
20
+ * "Stable newer version":http://github.com/technoweenie/restful-authentication/tree/master
21
+ * "'Classic' (backward-compatible) version":http://github.com/technoweenie/restful-authentication/tree/classic
22
+ * "Experimental version":http://github.com/technoweenie/restful-authentication/tree/modular (Much more modular, needs testing & review)
23
+
24
+ !! important: if you upgrade your site, existing user account !!
25
+ !! passwords will stop working unless you use --old-passwords !!
26
+
27
+ ***************************************************************************
28
+
29
+ h2. Issue Tracker
30
+
31
+ Please submit any bugs or annoyances on the lighthouse tracker at
32
+ * "http://rails_security.lighthouseapp.com/projects/15332-restful_authentication/overview":http://rails_security.lighthouseapp.com/projects/15332-restful_authentication/overview
33
+
34
+ For anything simple enough, please github message both maintainers: Rick Olson
35
+ ("technoweenie":http://github.com/technoweenie) and Flip Kromer
36
+ ("mrflip":http://github.com/mrflip).
37
+
38
+ ***************************************************************************
39
+
40
+ h2. Documentation
41
+
42
+ This page has notes on
43
+ * "Installation":#INSTALL
44
+ * "New Features":#AWESOME
45
+ * "After installing":#POST-INSTALL
46
+
47
+ See the "wiki":http://github.com/technoweenie/restful-authentication/wikis/home
48
+ (or the notes/ directory) if you want to learn more about:
49
+
50
+ * "Extensions, Addons and Alternatives":addons such as HAML templates
51
+ * "Security Design Patterns":security-patterns with "snazzy diagram":http://github.com/technoweenie/restful-authentication/tree/master/notes/SecurityFramework.png
52
+ * [[Authentication]] -- Lets a visitor identify herself (and lay claim to her corresponding Roles and measure of Trust)
53
+ * "Trust Metrics":Trustification -- Confidence we can rely on the outcomes of this visitor's actions.
54
+ * [[Authorization]] and Policy -- Based on trust and identity, what actions may this visitor perform?
55
+ * [[Access Control]] -- How the Authorization policy is actually enforced in your code (A: hopefully without turning it into a spaghetti of if thens)
56
+ * [[Rails Plugins]] for Authentication, Trust, Authorization and Access Control
57
+ * [[Tradeoffs]] -- for the paranoid or the curious, a rundown of tradeoffs made in the code
58
+ * [[CHANGELOG]] -- Summary of changes to internals
59
+ * [[TODO]] -- Ideas for how you can help
60
+
61
+ These best version of the release notes are in the notes/ directory in the
62
+ "source code":http://github.com/technoweenie/restful-authentication/tree/master
63
+ -- look there for the latest version. The wiki versions are taken (manually)
64
+ from there.
65
+
66
+ ***************************************************************************
67
+
68
+ <a id="AWESOME"/> </a>
69
+ h2. Exciting new features
70
+
71
+ h3. Stories
72
+
73
+ There are now "Cucumber":http://wiki.github.com/aslakhellesoy/cucumber/home features that allow expressive, enjoyable tests for the
74
+ authentication code. The flexible code for resource testing in stories was
75
+ extended from "Ben Mabey's.":http://www.benmabey.com/2008/02/04/rspec-plain-text-stories-webrat-chunky-bacon/
76
+
77
+ h3. Modularize to match security design patterns:
78
+
79
+ * Authentication (currently: password, browser cookie token, HTTP basic)
80
+ * Trust metric (email validation)
81
+ * Authorization (stateful roles)
82
+ * Leave a flexible framework that will play nicely with other access control / policy definition / trust metric plugins
83
+
84
+ h3. Other
85
+
86
+ * Added a few helper methods for linking to user pages
87
+ * Uniform handling of logout, remember_token
88
+ * Stricter email, login field validation
89
+ * Minor security fixes -- see CHANGELOG
90
+
91
+ ***************************************************************************
92
+
93
+ h2. Non-backwards compatible Changes
94
+
95
+ Here are a few changes in the May 2008 release that increase "Defense in Depth"
96
+ but may require changes to existing accounts
97
+
98
+ * If you have an existing site, none of these changes are compelling enough to
99
+ warrant migrating your userbase.
100
+ * If you are generating for a new site, all of these changes are low-impact.
101
+ You should apply them.
102
+
103
+ h3. Passwords
104
+
105
+ The new password encryption (using a site key salt and stretching) will break
106
+ existing user accounts' passwords. We recommend you use the --old-passwords
107
+ option or write a migration tool and submit it as a patch. See the
108
+ [[Tradeoffs]] note for more information.
109
+
110
+ h3. Validations
111
+
112
+ By default, email and usernames are validated against a somewhat strict pattern; your users' values may be now illegal. Adjust to suit.
113
+
114
+ ***************************************************************************
115
+
116
+ <a id="INSTALL"/> </a>
117
+ h2. Installation
118
+
119
+ This is a basic restful authentication generator for rails, taken from
120
+ acts as authenticated. Currently it requires Rails 1.2.6 or above.
121
+
122
+ **IMPORTANT FOR RAILS > 2.1 USERS** To avoid a @NameError@ exception ("lighthouse tracker ticket":http://rails_security.lighthouseapp.com/projects/15332-restful_authentication/tickets/2-not-a-valid-constant-name-errors#ticket-2-2), check out the code to have an _underscore_ and not _dash_ in its name:
123
+ * either use <code>git clone git://github.com/technoweenie/restful-authentication.git restful_authentication</code>
124
+ * or rename the plugin's directory to be <code>restful_authentication</code> after fetching it.
125
+
126
+ To use the generator:
127
+
128
+ ./script/generate authenticated user sessions \
129
+ --include-activation \
130
+ --stateful \
131
+ --rspec \
132
+ --skip-migration \
133
+ --skip-routes \
134
+ --old-passwords
135
+
136
+ * The first parameter specifies the model that gets created in signup (typically
137
+ a user or account model). A model with migration is created, as well as a
138
+ basic controller with the create method. You probably want to say "User" here.
139
+
140
+ * The second parameter specifies the session controller name. This is the
141
+ controller that handles the actual login/logout function on the site.
142
+ (probably: "Session").
143
+
144
+ * --include-activation: Generates the code for a ActionMailer and its respective
145
+ Activation Code through email.
146
+
147
+ * --stateful: Builds in support for acts_as_state_machine and generates
148
+ activation code. (@--stateful@ implies @--include-activation@). Based on the
149
+ idea at [[http://www.vaporbase.com/postings/stateful_authentication]]. Passing
150
+ @--skip-migration@ will skip the user migration, and @--skip-routes@ will skip
151
+ resource generation -- both useful if you've already run this generator.
152
+ (Needs the "acts_as_state_machine plugin":http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/,
153
+ but new installs should probably run with @--aasm@ instead.)
154
+
155
+ * --aasm: Works the same as stateful but uses the "updated aasm gem":http://github.com/rubyist/aasm/tree/master
156
+
157
+ * --rspec: Generate RSpec tests and Stories in place of standard rails tests.
158
+ This requires the
159
+ "RSpec and Rspec-on-rails plugins":http://rspec.info/
160
+ (make sure you "./script/generate rspec" after installing RSpec.) The rspec
161
+ and story suite are much more thorough than the rails tests, and changes are
162
+ unlikely to be backported.
163
+
164
+ * --old-passwords: Use the older password scheme (see [[#COMPATIBILITY]], above)
165
+
166
+ * --skip-migration: Don't generate a migration file for this model
167
+
168
+ * --skip-routes: Don't generate a resource line in @config/routes.rb@
169
+
170
+ ***************************************************************************
171
+ <a id="POST-INSTALL"/> </a>
172
+ h2. After installing
173
+
174
+ The below assumes a Model named 'User' and a Controller named 'Session'; please
175
+ alter to suit. There are additional security minutae in @notes/README-Tradeoffs@
176
+ -- only the paranoid or the curious need bother, though.
177
+
178
+ * Add these familiar login URLs to your @config/routes.rb@ if you like:
179
+
180
+ <pre><code>
181
+ map.signup '/signup', :controller => 'users', :action => 'new'
182
+ map.login '/login', :controller => 'session', :action => 'new'
183
+ map.logout '/logout', :controller => 'session', :action => 'destroy'
184
+ </code></pre>
185
+
186
+ * With @--include-activation@, also add to your @config/routes.rb@:
187
+
188
+ <pre><code>
189
+ map.activate '/activate/:activation_code', :controller => 'users', :action => 'activate', :activation_code => nil
190
+ </code></pre>
191
+
192
+ and add an observer to @config/environment.rb@:
193
+
194
+ <pre><code>
195
+ config.active_record.observers = :user_observer
196
+ </code></pre>
197
+
198
+ Pay attention, may be this is not an issue for everybody, but if you should
199
+ have problems, that the sent activation_code does match with that in the
200
+ database stored, reload your user object before sending its data through email
201
+ something like:
202
+
203
+ <pre><code>
204
+ class UserObserver < ActiveRecord::Observer
205
+ def after_create(user)
206
+ user.reload
207
+ UserMailer.deliver_signup_notification(user)
208
+ end
209
+ def after_save(user)
210
+ user.reload
211
+ UserMailer.deliver_activation(user) if user.recently_activated?
212
+ end
213
+ end
214
+ </code></pre>
215
+
216
+
217
+ * With @--stateful@, add an observer to config/environment.rb:
218
+
219
+ <pre><code>
220
+ config.active_record.observers = :user_observer
221
+ </code></pre>
222
+
223
+ and modify the users resource line to read
224
+
225
+ map.resources :users, :member => { :suspend => :put,
226
+ :unsuspend => :put,
227
+ :purge => :delete }
228
+
229
+ * If you use a public repository for your code (such as github, rubyforge,
230
+ gitorious, etc.) make sure to NOT post your site_keys.rb (add a line like
231
+ '/config/initializers/site_keys.rb' to your .gitignore or do the svn ignore
232
+ dance), but make sure you DO keep it backed up somewhere safe.
@@ -0,0 +1,54 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => :test
8
+
9
+ desc 'Test the restful_authentication plugin.'
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the restful_authentication plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'RestfulAuthentication'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ gemspec = eval(File.read("#{File.dirname(__FILE__)}/restful_authentication.gemspec"))
26
+ PKG_NAME = gemspec.name
27
+ PKG_VERSION = gemspec.version
28
+
29
+ Rake::GemPackageTask.new(gemspec) do |pkg|
30
+ pkg.need_zip = true
31
+ pkg.need_tar = true
32
+ end
33
+
34
+ desc "Update gemspec from existing one by regenerating path globs specified in *.gemspec.yml or defaults to liberal file globs."
35
+ task :gemspec_update do
36
+ if (gemspec_file = Dir['*.gemspec'][0])
37
+ original_gemspec = eval(File.read(gemspec_file))
38
+ if File.exists?("#{gemspec_file}.yml")
39
+ require 'yaml'
40
+ YAML::load_file("#{gemspec_file}.yml").each do |attribute, globs|
41
+ original_gemspec.send("#{attribute}=", FileList[globs])
42
+ end
43
+ else
44
+ # liberal defaults
45
+ original_gemspec.files = FileList["**/*"]
46
+ test_directories = original_gemspec.test_files.grep(/\//).map {|e| e[/^[^\/]+/]}.compact.uniq
47
+ original_gemspec.test_files = FileList["{#{test_directories.join(',')}}/**/*"] unless test_directories.empty?
48
+ end
49
+ File.open(gemspec_file, 'w') {|f| f.write(original_gemspec.to_ruby) }
50
+ puts "Updated gemspec."
51
+ else
52
+ puts "No existing gemspec file found."
53
+ end
54
+ end
data/TODO ADDED
@@ -0,0 +1,15 @@
1
+
2
+ h3. Authentication security projects for a later date
3
+
4
+
5
+ * Track 'failed logins this hour' and demand a captcha after say 5 failed logins
6
+ ("RECAPTCHA plugin.":http://agilewebdevelopment.com/plugins/recaptcha)
7
+ "De-proxy-ficate IP address": http://wiki.codemongers.com/NginxHttpRealIpModule
8
+
9
+ * Make cookie spoofing a little harder: we set the user's cookie to
10
+ (remember_token), but store digest(remember_token, request_IP). A CSRF cookie
11
+ spoofer has to then at least also spoof the user's originating IP
12
+ (see "Secure Programs HOWTO":http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/web-authentication.html)
13
+
14
+ * Log HTTP request on authentication / authorization failures
15
+ http://palisade.plynt.com/issues/2004Jul/safe-auth-practices
@@ -0,0 +1 @@
1
+ ./script/generate authenticated USERMODEL CONTROLLERNAME
@@ -0,0 +1,493 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/lib/insert_routes.rb")
2
+ require 'digest/sha1'
3
+
4
+ class AuthenticatedGenerator < Rails::Generator::NamedBase
5
+ default_options :skip_migration => false,
6
+ :skip_routes => false,
7
+ :old_passwords => false,
8
+ :include_activation => false
9
+
10
+ attr_reader :controller_name,
11
+ :controller_class_path,
12
+ :controller_file_path,
13
+ :controller_class_nesting,
14
+ :controller_class_nesting_depth,
15
+ :controller_class_name,
16
+ :controller_singular_name,
17
+ :controller_plural_name,
18
+ :controller_routing_name, # new_session_path
19
+ :controller_routing_path, # /session/new
20
+ :controller_controller_name, # sessions
21
+ :controller_file_name
22
+ alias_method :controller_table_name, :controller_plural_name
23
+ attr_reader :model_controller_name,
24
+ :model_controller_class_path,
25
+ :model_controller_file_path,
26
+ :model_controller_class_nesting,
27
+ :model_controller_class_nesting_depth,
28
+ :model_controller_class_name,
29
+ :model_controller_singular_name,
30
+ :model_controller_plural_name,
31
+ :model_controller_routing_name, # new_user_path
32
+ :model_controller_routing_path, # /users/new
33
+ :model_controller_controller_name # users
34
+ alias_method :model_controller_file_name, :model_controller_singular_name
35
+ alias_method :model_controller_table_name, :model_controller_plural_name
36
+
37
+ def initialize(runtime_args, runtime_options = {})
38
+ super
39
+
40
+ @rspec = has_rspec?
41
+
42
+ @controller_name = (args.shift || 'sessions').pluralize
43
+ @model_controller_name = @name.pluralize
44
+
45
+ # sessions controller
46
+ base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name)
47
+ @controller_class_name_without_nesting, @controller_file_name, @controller_plural_name = inflect_names(base_name)
48
+ @controller_singular_name = @controller_file_name.singularize
49
+ if @controller_class_nesting.empty?
50
+ @controller_class_name = @controller_class_name_without_nesting
51
+ else
52
+ @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
53
+ end
54
+ @controller_routing_name = @controller_singular_name
55
+ @controller_routing_path = @controller_file_path.singularize
56
+ @controller_controller_name = @controller_plural_name
57
+
58
+ # model controller
59
+ base_name, @model_controller_class_path, @model_controller_file_path, @model_controller_class_nesting, @model_controller_class_nesting_depth = extract_modules(@model_controller_name)
60
+ @model_controller_class_name_without_nesting, @model_controller_singular_name, @model_controller_plural_name = inflect_names(base_name)
61
+
62
+ if @model_controller_class_nesting.empty?
63
+ @model_controller_class_name = @model_controller_class_name_without_nesting
64
+ else
65
+ @model_controller_class_name = "#{@model_controller_class_nesting}::#{@model_controller_class_name_without_nesting}"
66
+ end
67
+ @model_controller_routing_name = @table_name
68
+ @model_controller_routing_path = @model_controller_file_path
69
+ @model_controller_controller_name = @model_controller_plural_name
70
+
71
+ load_or_initialize_site_keys
72
+
73
+ if options[:dump_generator_attribute_names]
74
+ dump_generator_attribute_names
75
+ end
76
+ end
77
+
78
+ def manifest
79
+ recorded_session = record do |m|
80
+ # Check for class naming collisions.
81
+ m.class_collisions controller_class_path, "#{controller_class_name}Controller", # Sessions Controller
82
+ "#{controller_class_name}Helper"
83
+ m.class_collisions model_controller_class_path, "#{model_controller_class_name}Controller", # Model Controller
84
+ "#{model_controller_class_name}Helper"
85
+ m.class_collisions class_path, "#{class_name}", "#{class_name}Mailer", "#{class_name}MailerTest", "#{class_name}Observer"
86
+ m.class_collisions [], 'AuthenticatedSystem', 'AuthenticatedTestHelper'
87
+
88
+ # Controller, helper, views, and test directories.
89
+ m.directory File.join('app/models', class_path)
90
+ m.directory File.join('app/controllers', controller_class_path)
91
+ m.directory File.join('app/controllers', model_controller_class_path)
92
+ m.directory File.join('app/helpers', controller_class_path)
93
+ m.directory File.join('app/views', controller_class_path, controller_file_name)
94
+ m.directory File.join('app/views', class_path, "#{file_name}_mailer") if options[:include_activation]
95
+
96
+ m.directory File.join('app/controllers', model_controller_class_path)
97
+ m.directory File.join('app/helpers', model_controller_class_path)
98
+ m.directory File.join('app/views', model_controller_class_path, model_controller_file_name)
99
+ m.directory File.join('config/initializers')
100
+ m.file 'tasks/auth.rake', 'lib/tasks/auth.rake'
101
+
102
+ if @rspec
103
+ m.directory File.join('spec/controllers', controller_class_path)
104
+ m.directory File.join('spec/controllers', model_controller_class_path)
105
+ m.directory File.join('spec/models', class_path)
106
+ m.directory File.join('spec/helpers', model_controller_class_path)
107
+ m.directory File.join('spec/blueprints', class_path)
108
+ m.directory 'features'
109
+ m.directory File.join('features', 'step_definitions')
110
+ else
111
+ m.directory File.join('test/functional', controller_class_path)
112
+ m.directory File.join('test/functional', model_controller_class_path)
113
+ m.directory File.join('test/unit', class_path)
114
+ m.directory File.join('test/blueprints', class_path)
115
+ end
116
+
117
+ m.template 'model.rb',
118
+ File.join('app/models',
119
+ class_path,
120
+ "#{file_name}.rb")
121
+
122
+ if options[:include_activation]
123
+ %w( mailer observer ).each do |model_type|
124
+ m.template "#{model_type}.rb", File.join('app/models',
125
+ class_path,
126
+ "#{file_name}_#{model_type}.rb")
127
+ end
128
+ end
129
+
130
+ m.template 'controller.rb',
131
+ File.join('app/controllers',
132
+ controller_class_path,
133
+ "#{controller_file_name}_controller.rb")
134
+
135
+ m.template 'model_controller.rb',
136
+ File.join('app/controllers',
137
+ model_controller_class_path,
138
+ "#{model_controller_file_name}_controller.rb")
139
+
140
+ m.template 'authenticated_system.rb',
141
+ File.join('lib', 'authenticated_system.rb')
142
+
143
+ m.template 'authenticated_test_helper.rb',
144
+ File.join('lib', 'authenticated_test_helper.rb')
145
+
146
+ m.template 'site_keys.rb', site_keys_file
147
+
148
+ if @rspec
149
+ # RSpec Specs
150
+ m.template 'spec/controllers/users_controller_spec.rb',
151
+ File.join('spec/controllers',
152
+ model_controller_class_path,
153
+ "#{model_controller_file_name}_controller_spec.rb")
154
+ m.template 'spec/controllers/sessions_controller_spec.rb',
155
+ File.join('spec/controllers',
156
+ controller_class_path,
157
+ "#{controller_file_name}_controller_spec.rb")
158
+ m.template 'spec/controllers/access_control_spec.rb',
159
+ File.join('spec/controllers',
160
+ controller_class_path,
161
+ "access_control_spec.rb")
162
+ m.template 'spec/controllers/authenticated_system_spec.rb',
163
+ File.join('spec/controllers',
164
+ controller_class_path,
165
+ "authenticated_system_spec.rb")
166
+ m.template 'spec/helpers/users_helper_spec.rb',
167
+ File.join('spec/helpers',
168
+ model_controller_class_path,
169
+ "#{table_name}_helper_spec.rb")
170
+ m.template 'spec/models/user_spec.rb',
171
+ File.join('spec/models',
172
+ class_path,
173
+ "#{file_name}_spec.rb")
174
+ m.template 'spec/blueprints/user.rb',
175
+ File.join('spec/blueprints',
176
+ class_path,
177
+ "#{file_name}.rb")
178
+
179
+ # Cucumber features
180
+ m.template 'features/step_definitions/user_steps.rb',
181
+ File.join('features/step_definitions/', "#{file_name}_steps.rb")
182
+ m.template 'features/accounts.feature',
183
+ File.join('features', 'accounts.feature')
184
+ m.template 'features/sessions.feature',
185
+ File.join('features', 'sessions.feature')
186
+ m.template 'features/step_definitions/ra_env.rb',
187
+ File.join('features', 'step_definitions', 'ra_env.rb')
188
+ m.template 'machinist_spec.rb',
189
+ File.join("config", "initializers", "machinist.rb")
190
+
191
+ else
192
+ m.template 'test/functional_test.rb',
193
+ File.join('test/functional',
194
+ controller_class_path,
195
+ "#{controller_file_name}_controller_test.rb")
196
+ m.template 'test/model_functional_test.rb',
197
+ File.join('test/functional',
198
+ model_controller_class_path,
199
+ "#{model_controller_file_name}_controller_test.rb")
200
+ m.template 'test/unit_test.rb',
201
+ File.join('test/unit',
202
+ class_path,
203
+ "#{file_name}_test.rb")
204
+ m.template 'spec/blueprints/user.rb',
205
+ File.join('test/blueprints',
206
+ class_path,
207
+ "#{file_name}.rb")
208
+ m.template 'machinist_test.rb',
209
+ File.join("config", "initializers", "machinist.rb")
210
+ if options[:include_activation]
211
+ m.template 'test/mailer_test.rb', File.join('test/unit', class_path, "#{file_name}_mailer_test.rb")
212
+ end
213
+ end
214
+
215
+ m.template 'helper.rb',
216
+ File.join('app/helpers',
217
+ controller_class_path,
218
+ "#{controller_file_name}_helper.rb")
219
+
220
+ m.template 'model_helper.rb',
221
+ File.join('app/helpers',
222
+ model_controller_class_path,
223
+ "#{model_controller_file_name}_helper.rb")
224
+
225
+
226
+ # Controller templates
227
+ m.template 'login.html.erb', File.join('app/views', controller_class_path, controller_file_name, "new.html.erb")
228
+ m.template 'signup.html.erb', File.join('app/views', model_controller_class_path, model_controller_file_name, "new.html.erb")
229
+ m.template '_model_partial.html.erb', File.join('app/views', model_controller_class_path, model_controller_file_name, "_#{file_name}_bar.html.erb")
230
+
231
+ if options[:include_activation]
232
+ # Mailer templates
233
+ %w( activation signup_notification ).each do |action|
234
+ m.template "#{action}.erb",
235
+ File.join('app/views', "#{file_name}_mailer", "#{action}.erb")
236
+ end
237
+ end
238
+
239
+ unless options[:skip_migration]
240
+ m.migration_template 'migration.rb', 'db/migrate', :assigns => {
241
+ :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}"
242
+ }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
243
+ end
244
+ unless options[:skip_routes]
245
+ # Note that this fails for nested classes -- you're on your own with setting up the routes.
246
+ m.route_resource controller_singular_name
247
+ if options[:stateful]
248
+ m.route_resources model_controller_plural_name, :member => { :suspend => :put,
249
+ :unsuspend => :put,
250
+ :purge => :delete }
251
+ else
252
+ m.route_resources model_controller_plural_name
253
+ end
254
+ m.route_name('signup', '/signup', {:controller => model_controller_plural_name, :action => 'new'})
255
+ m.route_name('register', '/register', {:controller => model_controller_plural_name, :action => 'create'})
256
+ m.route_name('login', '/login', {:controller => controller_controller_name, :action => 'new'})
257
+ m.route_name('logout', '/logout', {:controller => controller_controller_name, :action => 'destroy'})
258
+ if options[:include_activation]
259
+ m.route_name('activate', '/activate/:activation_code', { :controller => model_controller_plural_name, :action => 'activate', :activation_code => nil })
260
+ end
261
+ end
262
+ end
263
+
264
+ #
265
+ # Post-install notes
266
+ #
267
+ action = File.basename($0) # grok the action from './script/generate' or whatever
268
+ case action
269
+ when "generate"
270
+ puts "Ready to generate."
271
+ puts ("-" * 70)
272
+ puts "Once finished, don't forget to:"
273
+ puts
274
+ if options[:include_activation]
275
+ puts "- Add an observer to config/environment.rb"
276
+ puts " config.active_record.observers = :#{file_name}_observer"
277
+ end
278
+ if options[:aasm]
279
+ puts "- Install the acts_as_state_machine gem:"
280
+ puts " sudo gem sources -a http://gems.github.com (If you haven't already)"
281
+ puts " sudo gem install rubyist-aasm"
282
+ elsif options[:stateful]
283
+ puts "- Install the acts_as_state_machine plugin:"
284
+ puts " svn export http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk vendor/plugins/acts_as_state_machine"
285
+ end
286
+ puts
287
+ puts ("-" * 70)
288
+ puts
289
+ puts "- Add the following to your config/environments/cucumber.rb: "
290
+ puts
291
+ puts %( config.gem 'pickle')
292
+ puts
293
+ puts "- Add routes to these resources. In config/routes.rb, insert routes like:"
294
+ puts %( map.signup '/signup', :controller => '#{model_controller_file_name}', :action => 'new')
295
+ puts %( map.login '/login', :controller => '#{controller_file_name}', :action => 'new')
296
+ puts %( map.logout '/logout', :controller => '#{controller_file_name}', :action => 'destroy')
297
+ if options[:include_activation]
298
+ puts %( map.activate '/activate/:activation_code', :controller => '#{model_controller_file_name}', :action => 'activate', :activation_code => nil)
299
+ end
300
+ if options[:stateful]
301
+ puts " and modify the map.resources :#{model_controller_file_name} line to include these actions:"
302
+ puts " map.resources :#{model_controller_file_name}, :member => { :suspend => :put, :unsuspend => :put, :purge => :delete }"
303
+ end
304
+ puts
305
+ puts ("-" * 70)
306
+ puts
307
+ if $rest_auth_site_key_from_generator.blank?
308
+ puts "You've set a nil site key. This preserves existing users' passwords,"
309
+ puts "but allows dictionary attacks in the unlikely event your database is"
310
+ puts "compromised and your site code is not. See the README for more."
311
+ elsif $rest_auth_keys_are_new
312
+ puts "We've create a new site key in #{site_keys_file}. If you have existing"
313
+ puts "user accounts their passwords will no longer work (see README). As always,"
314
+ puts "keep this file safe but don't post it in public."
315
+ else
316
+ puts "We've reused the existing site key in #{site_keys_file}. As always,"
317
+ puts "keep this file safe but don't post it in public."
318
+ end
319
+ puts
320
+ puts ("-" * 70)
321
+ when "destroy"
322
+ puts
323
+ puts ("-" * 70)
324
+ puts
325
+ puts "Thanks for using restful_authentication"
326
+ puts
327
+ puts "Don't forget to comment out the observer line in environment.rb"
328
+ puts " (This was optional so it may not even be there)"
329
+ puts " # config.active_record.observers = :#{file_name}_observer"
330
+ puts
331
+ puts ("-" * 70)
332
+ puts
333
+ else
334
+ puts "Didn't understand the action '#{action}' -- you might have missed the 'after running me' instructions."
335
+ end
336
+
337
+ #
338
+ # Do the thing
339
+ #
340
+ recorded_session
341
+ end
342
+
343
+ def has_rspec?
344
+ spec_dir = File.join(RAILS_ROOT, 'spec')
345
+ options[:rspec] ||= (File.exist?(spec_dir) && File.directory?(spec_dir)) unless (options[:rspec] == false)
346
+ end
347
+
348
+ #
349
+ # !! These must match the corresponding routines in by_password.rb !!
350
+ #
351
+ def secure_digest(*args)
352
+ Digest::SHA1.hexdigest(args.flatten.join('--'))
353
+ end
354
+ def make_token
355
+ secure_digest(Time.now, (1..10).map{ rand.to_s })
356
+ end
357
+ def password_digest(password, salt)
358
+ digest = $rest_auth_site_key_from_generator
359
+ $rest_auth_digest_stretches_from_generator.times do
360
+ digest = secure_digest(digest, salt, password, $rest_auth_site_key_from_generator)
361
+ end
362
+ digest
363
+ end
364
+
365
+ #
366
+ # Try to be idempotent:
367
+ # pull in the existing site key if any,
368
+ # seed it with reasonable defaults otherwise
369
+ #
370
+ def load_or_initialize_site_keys
371
+ case
372
+ when defined? REST_AUTH_SITE_KEY
373
+ if (options[:old_passwords]) && ((! REST_AUTH_SITE_KEY.blank?) || (REST_AUTH_DIGEST_STRETCHES != 1))
374
+ raise "You have a site key, but --old-passwords will overwrite it. If this is really what you want, move the file #{site_keys_file} and re-run."
375
+ end
376
+ $rest_auth_site_key_from_generator = REST_AUTH_SITE_KEY
377
+ $rest_auth_digest_stretches_from_generator = REST_AUTH_DIGEST_STRETCHES
378
+ when options[:old_passwords]
379
+ $rest_auth_site_key_from_generator = nil
380
+ $rest_auth_digest_stretches_from_generator = 1
381
+ $rest_auth_keys_are_new = true
382
+ else
383
+ $rest_auth_site_key_from_generator = make_token
384
+ $rest_auth_digest_stretches_from_generator = 10
385
+ $rest_auth_keys_are_new = true
386
+ end
387
+ end
388
+
389
+ def site_keys_file
390
+ File.join("config", "initializers", "site_keys.rb")
391
+ end
392
+
393
+ protected
394
+ # Override with your own usage banner.
395
+ def banner
396
+ "Usage: #{$0} authenticated ModelName [ControllerName]"
397
+ end
398
+
399
+ def add_options!(opt)
400
+ opt.separator ''
401
+ opt.separator 'Options:'
402
+ opt.on("--skip-migration",
403
+ "Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
404
+ opt.on("--include-activation",
405
+ "Generate signup 'activation code' confirmation via email") { |v| options[:include_activation] = true }
406
+ opt.on("--stateful",
407
+ "Use acts_as_state_machine. Assumes --include-activation") { |v| options[:include_activation] = options[:stateful] = true }
408
+ opt.on("--aasm",
409
+ "Use (gem) aasm. Assumes --include-activation") { |v| options[:include_activation] = options[:stateful] = options[:aasm] = true }
410
+ opt.on("--rspec",
411
+ "Force rspec mode (checks for RAILS_ROOT/spec by default)") { |v| options[:rspec] = true }
412
+ opt.on("--no-rspec",
413
+ "Force test (not RSpec mode") { |v| options[:rspec] = false }
414
+ opt.on("--skip-routes",
415
+ "Don't generate a resource line in config/routes.rb") { |v| options[:skip_routes] = v }
416
+ opt.on("--old-passwords",
417
+ "Use the older password encryption scheme (see README)") { |v| options[:old_passwords] = v }
418
+ opt.on("--dump-generator-attrs",
419
+ "(generator debug helper)") { |v| options[:dump_generator_attribute_names] = v }
420
+ end
421
+
422
+ def dump_generator_attribute_names
423
+ generator_attribute_names = [
424
+ :table_name,
425
+ :file_name,
426
+ :class_name,
427
+ :controller_name,
428
+ :controller_class_path,
429
+ :controller_file_path,
430
+ :controller_class_nesting,
431
+ :controller_class_nesting_depth,
432
+ :controller_class_name,
433
+ :controller_singular_name,
434
+ :controller_plural_name,
435
+ :controller_routing_name, # new_session_path
436
+ :controller_routing_path, # /session/new
437
+ :controller_controller_name, # sessions
438
+ :controller_file_name,
439
+ :controller_table_name, :controller_plural_name,
440
+ :model_controller_name,
441
+ :model_controller_class_path,
442
+ :model_controller_file_path,
443
+ :model_controller_class_nesting,
444
+ :model_controller_class_nesting_depth,
445
+ :model_controller_class_name,
446
+ :model_controller_singular_name,
447
+ :model_controller_plural_name,
448
+ :model_controller_routing_name, # new_user_path
449
+ :model_controller_routing_path, # /users/new
450
+ :model_controller_controller_name, # users
451
+ :model_controller_file_name, :model_controller_singular_name,
452
+ :model_controller_table_name, :model_controller_plural_name,
453
+ ]
454
+ generator_attribute_names.each do |attr|
455
+ puts "%-40s %s" % ["#{attr}:", self.send(attr)] # instance_variable_get("@#{attr.to_s}"
456
+ end
457
+
458
+ end
459
+ end
460
+
461
+ # ./script/generate authenticated FoonParent::Foon SporkParent::Spork -p --force --rspec --dump-generator-attrs
462
+ # table_name: foon_parent_foons
463
+ # file_name: foon
464
+ # class_name: FoonParent::Foon
465
+ # controller_name: SporkParent::Sporks
466
+ # controller_class_path: spork_parent
467
+ # controller_file_path: spork_parent/sporks
468
+ # controller_class_nesting: SporkParent
469
+ # controller_class_nesting_depth: 1
470
+ # controller_class_name: SporkParent::Sporks
471
+ # controller_singular_name: spork
472
+ # controller_plural_name: sporks
473
+ # controller_routing_name: spork
474
+ # controller_routing_path: spork_parent/spork
475
+ # controller_controller_name: sporks
476
+ # controller_file_name: sporks
477
+ # controller_table_name: sporks
478
+ # controller_plural_name: sporks
479
+ # model_controller_name: FoonParent::Foons
480
+ # model_controller_class_path: foon_parent
481
+ # model_controller_file_path: foon_parent/foons
482
+ # model_controller_class_nesting: FoonParent
483
+ # model_controller_class_nesting_depth: 1
484
+ # model_controller_class_name: FoonParent::Foons
485
+ # model_controller_singular_name: foons
486
+ # model_controller_plural_name: foons
487
+ # model_controller_routing_name: foon_parent_foons
488
+ # model_controller_routing_path: foon_parent/foons
489
+ # model_controller_controller_name: foons
490
+ # model_controller_file_name: foons
491
+ # model_controller_singular_name: foons
492
+ # model_controller_table_name: foons
493
+ # model_controller_plural_name: foons