sparkly-auth 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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