sparkly-auth 1.0.0

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 (71) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +17 -0
  3. data/Rakefile +50 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/sparkly_accounts_controller.rb +59 -0
  6. data/app/controllers/sparkly_controller.rb +47 -0
  7. data/app/controllers/sparkly_sessions_controller.rb +52 -0
  8. data/app/models/password.rb +3 -0
  9. data/app/models/remembrance_token.rb +50 -0
  10. data/app/views/sparkly_accounts/edit.html.erb +24 -0
  11. data/app/views/sparkly_accounts/new.html.erb +24 -0
  12. data/app/views/sparkly_accounts/show.html.erb +0 -0
  13. data/app/views/sparkly_sessions/new.html.erb +22 -0
  14. data/dependencies.rb +1 -0
  15. data/generators/sparkly/USAGE +27 -0
  16. data/generators/sparkly/sparkly_generator.rb +76 -0
  17. data/generators/sparkly/templates/accounts_controller.rb +65 -0
  18. data/generators/sparkly/templates/accounts_helper.rb +2 -0
  19. data/generators/sparkly/templates/help_file.txt +56 -0
  20. data/generators/sparkly/templates/initializer.rb +30 -0
  21. data/generators/sparkly/templates/migrations/add_confirmed_to_sparkly_passwords.rb +9 -0
  22. data/generators/sparkly/templates/migrations/create_sparkly_passwords.rb +19 -0
  23. data/generators/sparkly/templates/migrations/create_sparkly_remembered_tokens.rb +15 -0
  24. data/generators/sparkly/templates/sessions_controller.rb +45 -0
  25. data/generators/sparkly/templates/sessions_helper.rb +2 -0
  26. data/generators/sparkly/templates/tasks/migrations.rb +1 -0
  27. data/generators/sparkly/templates/views/sparkly_accounts/edit.html.erb +24 -0
  28. data/generators/sparkly/templates/views/sparkly_accounts/new.html.erb +24 -0
  29. data/generators/sparkly/templates/views/sparkly_accounts/show.html.erb +0 -0
  30. data/generators/sparkly/templates/views/sparkly_sessions/new.html.erb +22 -0
  31. data/init.rb +44 -0
  32. data/lib/auth.rb +52 -0
  33. data/lib/auth/behavior/base.rb +64 -0
  34. data/lib/auth/behavior/core.rb +87 -0
  35. data/lib/auth/behavior/core/authenticated_model_methods.rb +52 -0
  36. data/lib/auth/behavior/core/controller_extensions.rb +52 -0
  37. data/lib/auth/behavior/core/controller_extensions/class_methods.rb +24 -0
  38. data/lib/auth/behavior/core/controller_extensions/current_user.rb +54 -0
  39. data/lib/auth/behavior/core/password_methods.rb +65 -0
  40. data/lib/auth/behavior/remember_me.rb +17 -0
  41. data/lib/auth/behavior/remember_me/configuration.rb +21 -0
  42. data/lib/auth/behavior/remember_me/controller_extensions.rb +66 -0
  43. data/lib/auth/behavior_lookup.rb +10 -0
  44. data/lib/auth/configuration.rb +328 -0
  45. data/lib/auth/encryptors/sha512.rb +20 -0
  46. data/lib/auth/generators/configuration_generator.rb +20 -0
  47. data/lib/auth/generators/controllers_generator.rb +34 -0
  48. data/lib/auth/generators/migration_generator.rb +32 -0
  49. data/lib/auth/generators/route_generator.rb +19 -0
  50. data/lib/auth/generators/views_generator.rb +26 -0
  51. data/lib/auth/model.rb +94 -0
  52. data/lib/auth/observer.rb +21 -0
  53. data/lib/auth/target_list.rb +5 -0
  54. data/lib/auth/tasks/migrations.rb +71 -0
  55. data/lib/auth/token.rb +10 -0
  56. data/lib/sparkly-auth.rb +1 -0
  57. data/rails/init.rb +17 -0
  58. data/rails/routes.rb +19 -0
  59. data/sparkly-auth.gemspec +143 -0
  60. data/spec/controllers/application_controller_spec.rb +13 -0
  61. data/spec/generators/sparkly_spec.rb +64 -0
  62. data/spec/lib/auth/behavior/core_spec.rb +184 -0
  63. data/spec/lib/auth/behavior/remember_me_spec.rb +127 -0
  64. data/spec/lib/auth/extensions/controller_spec.rb +32 -0
  65. data/spec/lib/auth/model_spec.rb +57 -0
  66. data/spec/lib/auth_spec.rb +32 -0
  67. data/spec/mocks/models/user.rb +3 -0
  68. data/spec/routes_spec.rb +24 -0
  69. data/spec/spec_helper.rb +61 -0
  70. data/spec/views_spec.rb +18 -0
  71. metadata +210 -0
@@ -0,0 +1,10 @@
1
+ module Auth::BehaviorLookup
2
+ def lookup_behavior(behavior)
3
+ name = behavior.to_s.underscore
4
+ if name[/^auth\/behavior\//]
5
+ behavior.to_s.camelize.constantize
6
+ else
7
+ "auth/behavior/#{name}".camelize.constantize
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,328 @@
1
+ module Auth
2
+ class Configuration
3
+ include Auth::BehaviorLookup
4
+
5
+ class << self
6
+ include Auth::BehaviorLookup
7
+
8
+ def behavior_configs
9
+ @behavior_configs ||= []
10
+ end
11
+
12
+ def register_behavior(name)
13
+ behavior_class = lookup_behavior(name)
14
+ # If the behavior has a configuration, add it to self.
15
+ accessor_name = name
16
+ name = "#{behavior_class.name}::Configuration"
17
+ behavior_configs << [ accessor_name, name.constantize ]
18
+ # eg Auth.remember_me.something = 5
19
+ Auth.class.delegate accessor_name, :to => :configuration
20
+ rescue NameError
21
+ # Presumably, the behavior does not have a configuration.
22
+ end
23
+ end
24
+
25
+ # IS there a better way to do this?? I'm dying to find it...
26
+ Dir[File.join(File.dirname(__FILE__), "behavior/*.rb")].each do |fi|
27
+ unless fi[/\/base.rb$/]
28
+ const_name = fi.gsub(/^#{Regexp::escape File.dirname(__FILE__)}\/behavior\/(.*)\.rb$/, '\1')
29
+ register_behavior(const_name)
30
+ end
31
+ end
32
+
33
+ # The message to display when the user creates an account.
34
+ #
35
+ # Default:
36
+ # "Your account has been created."
37
+ attr_accessor :account_created_message
38
+
39
+ # The message to display when the user deletes his or her account.
40
+ #
41
+ # Default:
42
+ # "Your account has been deleted."
43
+ attr_accessor :account_deleted_message
44
+
45
+ # The message to display when user profile has been updated or the password has been changed.
46
+ #
47
+ # Default:
48
+ # "Your changes have been saved."
49
+ attr_accessor :account_updated_message
50
+
51
+ # The length of time an account is locked for, if it is locked.
52
+ #
53
+ # Default:
54
+ # 30.minutes
55
+ attr_accessor :account_lock_duration
56
+
57
+ # The message to display if an account has been locked.
58
+ #
59
+ # Default:
60
+ # "Account is locked due to too many invalid attempts."
61
+ attr_accessor :account_locked_message
62
+
63
+ # The array of Auth::Model instances which represent the models which will be authenticated.
64
+ # See also #authenticate
65
+ attr_accessor :authenticated_models
66
+
67
+ # The NAME of the controller to use as a base controller. All Sparkly controllers will subclass
68
+ # this, and methods such as current_user will be added to it. Defaults to 'application'.
69
+ #
70
+ # Default:
71
+ # 'application'
72
+ attr_accessor :base_controller_name
73
+
74
+ # The array of behaviors which will be applied by default to every authenticated model. If
75
+ # a behavior set is specified for a given model, it will be used instead of (not in addition to)
76
+ # this array.
77
+ #
78
+ # Default:
79
+ # [ :core ]
80
+ attr_accessor :behaviors
81
+
82
+ # The name of the controller to route to for creating users, editing them, etc.
83
+ #
84
+ # "sparkly_accounts"
85
+ attr_accessor :default_accounts_controller_name
86
+
87
+ # If an issue would prevent the user from viewing the current page, Auth will redirect the user
88
+ # to the value stored in session[:destination]. If this value is not set, then Auth will default
89
+ # to this path.
90
+ #
91
+ # Default:
92
+ # "/"
93
+ attr_accessor :default_destination
94
+
95
+ # The method to call in order to determine which resource to use when implicitly logging in.
96
+ #
97
+ # If set to nil, the #default_destination will be used instead.
98
+ #
99
+ # Default:
100
+ # :new_user_session_path
101
+ attr_accessor :default_login_path
102
+
103
+ # The name of the controller to route to for logging in, logging out, etc.
104
+ #
105
+ # Default:
106
+ # "sparkly_sessions"
107
+ attr_accessor :default_sessions_controller_name
108
+
109
+ # The class to use for encryption of passwords. This can be any class, as long as it responds
110
+ # to #encrypt and #matches?
111
+ #
112
+ # Default:
113
+ # Auth::Encryptors::Sha512
114
+ attr_accessor :encryptor
115
+
116
+ # Message to display if username and/or password were incorrect.
117
+ #
118
+ # Default:
119
+ # "Credentials were not valid."
120
+ attr_accessor :invalid_credentials_message
121
+
122
+ # If true, the user will be automatically logged in after registering a new account.
123
+ # Note that this can be modified by some behaviors.
124
+ #
125
+ # Default:
126
+ # true
127
+ attr_accessor :login_after_signup
128
+
129
+ # The message to display when the user is not allowed to view a page because s/he must log in.
130
+ #
131
+ # Default:
132
+ # "You must be signed in to view this page."
133
+ attr_accessor :login_required_message
134
+
135
+ # Message to display if login was successful.
136
+ #
137
+ # Default:
138
+ # "Signed in successfully."
139
+ attr_accessor :login_successful_message
140
+
141
+ # Message to display when user logs out.
142
+ #
143
+ # Default:
144
+ # "You have been signed out."
145
+ attr_accessor :logout_message
146
+
147
+ # The message to display when the user is not allowed to view a page because s/he must log out.
148
+ #
149
+ # "You must be signed out to view this page."
150
+ attr_accessor :logout_required_message
151
+
152
+ # The maximum login attempts permitted before an account is locked. Set to nil to disable locking.
153
+ #
154
+ # Default:
155
+ # 5
156
+ attr_accessor :max_login_failures
157
+
158
+ # Minimum length for passwords.
159
+ #
160
+ # Default:
161
+ # 7
162
+ attr_accessor :minimum_password_length
163
+
164
+ # Regular expression which passwords must match. The default forces at least 1
165
+ # uppercase, lowercase and numeric character.
166
+ #
167
+ # Default:
168
+ # /(^(?=.*\d)(?=.*[a-zA-Z]).{7,}$)/
169
+ attr_accessor :password_format
170
+
171
+ # When the password to be created does not conform to the above format, this error
172
+ # message will be shown.
173
+ #
174
+ # Default:
175
+ # "must contain at least 1 uppercase, 1 lowercase and 1 number"
176
+ attr_accessor :password_format_message
177
+
178
+ # The number of passwords to keep in the password change history for each user. Any given
179
+ # user may not use the same password twice for at least this duration. For instance, if
180
+ # set to 4, then a user must change his password 4 times before s/he can reuse one of
181
+ # his/her previous passwords.
182
+ #
183
+ # Default:
184
+ # 4
185
+ attr_accessor :password_history_length
186
+
187
+ # The message to display when password change matches one of the previous passwords
188
+ #
189
+ # Default:
190
+ # "must not be the same as any of your recent passwords"
191
+ attr_accessor :password_uniqueness_message
192
+
193
+ # How frequently should passwords be forced to change? Nil for never.
194
+ #
195
+ # Default:
196
+ # 30.days
197
+ attr_accessor :password_update_frequency
198
+
199
+ # The path to the Sparkly Auth libraries.
200
+ attr_reader :path
201
+
202
+ # The maximum session duration. Users will be logged out automatically after this period expires.
203
+ #
204
+ # Default:
205
+ # 30.minutes
206
+ attr_accessor :session_duration
207
+
208
+ # Message to display when the user's session times out due to inactivity.
209
+ #
210
+ # Default:
211
+ # "You have been signed out due to inactivity. Please sign in again."
212
+ attr_accessor :session_timeout_message
213
+
214
+ # Finds the controller with the same name as #base_controller_name and returns it.
215
+ def base_controller
216
+ "#{base_controller_name.to_s.camelize}Controller".constantize
217
+ rescue NameError => err
218
+ begin
219
+ base_controller_name.to_s.camelize.constantize
220
+ rescue NameError
221
+ raise err
222
+ end
223
+ end
224
+
225
+ # Returns the classes which represent each behavior listed in #behaviors
226
+ def behavior_classes
227
+ behaviors.collect { |behavior| lookup_behavior(behavior) }
228
+ end
229
+
230
+ # Causes Sparkly Auth to *not* generate routes by default. You'll have to map them yourself if you disable
231
+ # route generation.
232
+ def disable_route_generation!
233
+ @generate_routes = false
234
+ end
235
+
236
+ # Returns true if Sparkly Auth is expected to generate routes for this application. This is true by
237
+ # default, and can be disabled with #disable_route_generation!
238
+ def generate_routes?
239
+ @generate_routes
240
+ end
241
+
242
+ def initialize
243
+ @password_format = /(^(?=.*\d)(?=.*[a-zA-Z]).{7,}$)/
244
+ @password_format_message = "must contain at least 1 uppercase, 1 lowercase and 1 number"
245
+ @minimum_password_length = 7
246
+ @path = File.expand_path(File.join(File.dirname(__FILE__), '..'))
247
+ @authenticated_models = Auth::TargetList.new
248
+ @behaviors = [ :core ]
249
+ @password_update_frequency = 30.days
250
+ @encryptor = Auth::Encryptors::Sha512
251
+ @password_uniqueness_message = "must not be the same as any of your recent passwords"
252
+ @password_history_length = 4
253
+ @default_accounts_controller_name = "sparkly_accounts"
254
+ @default_sessions_controller_name = "sparkly_sessions"
255
+ @login_required_message = "You must be signed in to view this page."
256
+ @logout_required_message = "You must be signed out to view this page."
257
+ @invalid_credentials_message = "Credentials were not valid."
258
+ @login_successful_message = "Signed in successfully."
259
+ @default_destination = "/"
260
+ @base_controller_name = 'application'
261
+ @session_duration = 30.minutes
262
+ @logout_message = "You have been signed out."
263
+ @session_timeout_message = "You have been signed out due to inactivity. Please sign in again."
264
+ @default_login_path = :new_user_session_path
265
+ @account_deleted_message = "Your account has been deleted."
266
+ @account_created_message = "Your account has been created."
267
+ @account_updated_message = "Your changes have been saved."
268
+ @account_locked_message = "Account is locked due to too many invalid attempts."
269
+ @account_lock_duration = 30.minutes
270
+ @max_login_failures = 5
271
+ @generate_routes = true
272
+ @login_after_signup = false
273
+
274
+ self.class.behavior_configs.each do |accessor_name, config_klass|
275
+ instance_variable_set("@#{accessor_name}", config_klass.new(self))
276
+ singleton = (class << self; self; end)
277
+ singleton.send(:define_method, accessor_name) { instance_variable_get("@#{accessor_name}") }
278
+ end
279
+ end
280
+
281
+ def apply!
282
+ # Apply behaviors to controllers
283
+ behaviors.each do |behavior_name|
284
+ behavior = lookup_behavior(behavior_name)
285
+ behavior.apply_to_controllers
286
+ end
287
+
288
+ # Apply options to authenticated models
289
+ authenticated_models.each do |model|
290
+ model.apply_options!
291
+ end
292
+ end
293
+
294
+ # Accepts a list of model names (or the models themselves) and an optional set of options which
295
+ # govern how the models will be authenticated.
296
+ #
297
+ # Examples:
298
+ # Auth.configure do |config|
299
+ # config.authenticate :user
300
+ # config.authenticate :admin, :key => :login
301
+ # config.authenticate :user, :admin, :with => /a password validating regexp/
302
+ # end
303
+ #
304
+ # Note that if an item is specified more than once, the options will be merged together for the
305
+ # entry. For instance, in the above example, the :user model will be authenticated with :password,
306
+ # while the :admin model will be authenticated with :password on key :login.
307
+ #
308
+ def authenticate(*model_names)
309
+ options = model_names.extract_options!
310
+ model_names.flatten.each do |name|
311
+ if model = authenticated_models.find(name)
312
+ model.merge_options! options
313
+ else
314
+ authenticated_models << Auth::Model.new(name, options)
315
+ end
316
+ end
317
+ end
318
+
319
+ # Returns the configuration for the given authenticated model.
320
+ def for_model(name_or_class_or_instance)
321
+ name_or_class = name_or_class_or_instance
322
+ name_or_class = name_or_class.class if name_or_class.kind_of?(ActiveRecord::Base)
323
+ authenticated_models.find(name_or_class)
324
+ end
325
+
326
+ private
327
+ end
328
+ end
@@ -0,0 +1,20 @@
1
+ class Auth::Encryptors::Sha512
2
+ class << self
3
+ attr_accessor :token_delimeter
4
+ attr_writer :stretches
5
+
6
+ def stretches
7
+ @stretches ||= 20
8
+ end
9
+
10
+ def encrypt(*what)
11
+ digest = what.flatten.join(token_delimeter)
12
+ stretches.times { digest = Digest::SHA512.hexdigest(digest) }
13
+ digest
14
+ end
15
+
16
+ def matches?(encrypted_copy, *what)
17
+ encrypt(*what) == encrypted_copy
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ class Auth::Generators::ConfigurationGenerator < Rails::Generator::Base
2
+ attr_reader :model
3
+
4
+ def initialize(args, options = {})
5
+ super(args, options)
6
+ end
7
+
8
+ def manifest
9
+ record do |m|
10
+ m.directory "lib/tasks"
11
+ m.directory "config/initializers"
12
+ m.file "tasks/migrations.rb", "lib/tasks/sparkly_migration.rb"
13
+ m.file 'initializer.rb', 'config/initializers/sparkly_authentication.rb'
14
+ end
15
+ end
16
+
17
+ def spec
18
+ @spec ||= Rails::Generator::Spec.new("configuration", File.join(Auth.path, "auth/generators"), nil)
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ class Auth::Generators::ControllersGenerator < Rails::Generator::NamedBase
2
+ attr_reader :model
3
+
4
+ def initialize(model, options = {})
5
+ @model = model
6
+ args = [ model.name ]
7
+ super(args, options)
8
+ end
9
+
10
+ def manifest
11
+ record do |m|
12
+ m.directory "app/controllers"
13
+ m.directory "app/helpers"
14
+
15
+ m.template "accounts_controller.rb", File.join("app/controllers", "#{model.accounts_controller.underscore}_controller.rb"),
16
+ :assigns => { :model => model }
17
+ m.template "sessions_controller.rb", File.join("app/controllers", "#{model.sessions_controller.underscore}_controller.rb"),
18
+ :assigns => { :model => model }
19
+
20
+ m.template "accounts_helper.rb", File.join("app/helpers", "#{model.accounts_controller.underscore}_helper.rb"),
21
+ :assigns => { :model => model }
22
+ m.template "sessions_helper.rb", File.join("app/helpers", "#{model.sessions_controller.underscore}_helper.rb"),
23
+ :assigns => { :model => model }
24
+
25
+ # Controller generator should also kick off the corresponding view generation.
26
+ views = Auth::Generators::ViewsGenerator.new(model, :source => File.join(source_root), :destination => destination_root)
27
+ views.manifest.replay(m)
28
+ end
29
+ end
30
+
31
+ def spec
32
+ @spec ||= Rails::Generator::Spec.new("controllers", File.join(Auth.path, "auth/generators"), nil)
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ class Auth::Generators::MigrationGenerator < Rails::Generator::NamedBase
2
+ attr_reader :model
3
+
4
+ def initialize(model, options = {})
5
+ @model = model
6
+ args = [ model.name ]
7
+ super(args, options)
8
+ end
9
+
10
+ def manifest
11
+ record do |m|
12
+ m.directory "db/migrate"
13
+ mg_version = 0
14
+ Auth.behavior_classes.each do |behavior|
15
+ behavior.migrations.each do |file_name|
16
+ fn_with_ext = file_name[/\.([^\.]+)$/] ? file_name : "#{file_name}.rb"
17
+ mg_version += 1
18
+ mg_version_s = mg_version.to_s.rjust(3, '0')
19
+ m.template File.join("migrations", fn_with_ext), File.join("db/migrate/#{mg_version_s}_#{fn_with_ext}")
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def table_name
26
+ model && model.target ? model.target.table_name : super
27
+ end
28
+
29
+ def spec
30
+ @spec ||= Rails::Generator::Spec.new("sparkly_migration", File.join(Auth.path, "auth/generators"), nil)
31
+ end
32
+ end