model_security_generator 0.0.7 → 0.0.8

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.
@@ -18,6 +18,7 @@ class ModelSecurityGenerator < Rails::Generator::NamedBase
18
18
 
19
19
  # User
20
20
  m.file "models/user.rb", "app/models/user.rb"
21
+ m.file "models/user_configuration.rb", "app/models/user_configuration.rb"
21
22
  m.file "controllers/user_controller.rb", "app/controllers/user_controller.rb"
22
23
 
23
24
  # User mailer
@@ -33,7 +34,9 @@ class ModelSecurityGenerator < Rails::Generator::NamedBase
33
34
 
34
35
  # Schemas, configuration and miscellaneous
35
36
  m.file "db/demo.sql", "db/demo.sql"
37
+ m.file "db/schema.sql", "db/schema.sql"
36
38
  m.file "db/users.sql", "db/users.sql"
39
+ m.file "db/user_configurations.sql", "db/user_configurations.sql"
37
40
 
38
41
  # Layout and stylesheet.
39
42
 
@@ -54,13 +57,14 @@ class ModelSecurityGenerator < Rails::Generator::NamedBase
54
57
  m.file "README", "manuals/ModelSecurity/README"
55
58
  m.file "USAGE", "manuals/ModelSecurity/USAGE"
56
59
  m.file "RUN_DEMO", "manuals/ModelSecurity/RUN_DEMO"
60
+ m.file "Tutorial.html", "manuals/ModelSecurity/Tutorial.html"
57
61
  m.file "ADD_TO_APPLICATION_CONTROLLER",
58
62
  "app/controllers/ADD_TO_APPLICATION_CONTROLLER.ModelSecurity"
59
63
  end
60
64
  end
61
65
 
62
66
  def user_views
63
- %w(activate admin_created created forgot_password forgot_password_done login login_admin logout new success)
67
+ %w(activate admin_created configure created forgot_password forgot_password_done list login logout new success)
64
68
  end
65
69
 
66
70
  def mailer_views
@@ -0,0 +1,669 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+
4
+ <html xmlns="http://www.w3.org/1999/xhtml">
5
+ <head>
6
+ <meta name="generator" content=
7
+ "HTML Tidy for Linux/x86 (vers 12 April 2005), see www.w3.org" />
8
+
9
+ <title>Securing your Rails models with ModelSecurity</title>
10
+ </head>
11
+
12
+ <body>
13
+ <h1>Securing your Rails models with ModelSecurity.</h1><i>Bruce
14
+ Perens<br />
15
+ <a href="mailto:bruce@perens.com">bruce@perens.com</a><br />
16
+ Vice President, Sourcelabs.</i><br />
17
+ Last modified: Wed Oct 5 14:26:14 PDT 2005<br />
18
+
19
+ <p><b>ModelSecurity</b> helps Ruby on Rails developers implement
20
+ a security <i>defense in depth</i> by implementing access control
21
+ within the data model.</p>
22
+
23
+ <p>If you are like most developers, you think about security when
24
+ you program controllers and views. But a bug in your controller
25
+ or view can compromise the security of your application, unless
26
+ your data model has <i>also</i> been secured.</p>
27
+
28
+ <p>The economical, flexible, and extremely readable means of
29
+ specifying access controls provided by ModelSecurity makes it
30
+ easier for the developer to <i>think</i> about security, and
31
+ makes security assumptions that might otherwise live in one
32
+ developers head <i>concrete</i> and <i>communicable to
33
+ others.</i></p>
34
+
35
+ <p><i>Defense in depth</i> is a common military strategy to
36
+ prevent an attack from succeeding. If the attacker gets through
37
+ the first level of defense, there's another level there to meet
38
+ him. Defense in depth is useful to programmers because it will
39
+ reveal a flaw for repair in the first level while simultaneously
40
+ stopping the attack at the second level. Most developers
41
+ implement some security features in their controllers and views:
42
+ for example, a controller will generally not allow access to
43
+ administrative features unless the user is logged in as an
44
+ administrator, and your views should not display data that is
45
+ inappropriate for a particular user. This is the "first layer of
46
+ defense". At that point, we come to the data model access
47
+ permissions: your second layer of defense. If you've specified
48
+ them, ActiveRecord will not allow the data to be revealed or
49
+ modified in an inappropriate manner. Views will in general
50
+ quietly fail to present non-permitted material in a way that can
51
+ be exploited: it will be blank if it's not readable, and will be
52
+ presented as static data rather than in an edit field if it's not
53
+ writable. This behavior facilitates scaffolding, which would
54
+ display and allow modification of everything if not told
55
+ otherwise. A non-permitted controller access to the data model
56
+ will result in a <code>SecurityViolation</code> exception being
57
+ raised, which causes the action to terminate and logs diagnostic
58
+ information that will assist the developer to locate the
59
+ controller bug.</p>
60
+
61
+ <p>ModelSecurity adds these facilities to Rails:</p>
62
+
63
+ <ul>
64
+ <li><code>let_read,</code> <code>let_write</code>, and
65
+ <code>let_access</code> are added to model declarations. They
66
+ allow fine-grained security permissions to be specified for
67
+ each model attribute, or for all of them at once.</li>
68
+
69
+ <li>Security conditions are specified as blocks or the names of
70
+ functions. Each returns a boolean, and <code>true</code>
71
+ indicates that the access should proceed while
72
+ <code>false</code> causes it to be denied. This mechanism
73
+ allows any user authorization system to be wired into
74
+ ModelSecurity with only a few statements.</li>
75
+
76
+ <li>The facilities that are most commonly used to write Rails
77
+ views, forms, and scaffolds heed the data model security
78
+ settings. They will not present or allow the modification of an
79
+ ActiveRecord attribute if that action is prohibited by the
80
+ access controls on the data model.</li>
81
+
82
+ <li>ModelSecurity provides a <code>let_display</code> facility
83
+ that works similarly to the security specifications, and tells
84
+ Rails scaffolds and views what fields of a data model are
85
+ "interesting" and what fields need not be shown to a particular
86
+ user.</li>
87
+
88
+ <li>A simple <b>User</b> model with a login facility, already
89
+ configured to work with ModelSecurity, is part of the
90
+ package.</li>
91
+
92
+ <li>The <b>UserSupport</b> controller mixin provides a login
93
+ facility with HTTP authentication to the entire application:
94
+
95
+ <ul>
96
+ <li><code>User_setup</code> is a before-filter for the
97
+ entire application. It handles login control and session
98
+ management without additional attention from the
99
+ programmer.</li>
100
+
101
+ <li>The before-filters <code>require_login</code> and
102
+ <code>require_admin</code> ensure that a user logs in with
103
+ proper privilege and then will complete the requested
104
+ action.</li>
105
+ </ul>
106
+ </li>
107
+
108
+ <li>
109
+ <b>Modal</b>, a means to handle two common interactions in
110
+ web design:
111
+
112
+ <ul>
113
+ <li>Modal web forms such as login dialogues that
114
+ "interrupt" an action and then allow it to complete after
115
+ the form has been submitted.</li>
116
+
117
+ <li>Modal forms that are called from a page to which they
118
+ will later return the user, such as comment forms within a
119
+ weblog.</li>
120
+ </ul>
121
+ </li>
122
+ </ul>
123
+
124
+ <h2>Getting Started</h2>
125
+
126
+ <p>Before we go through the features of ModelSecuity, it's useful
127
+ to get working code on your system. To go through this demo,
128
+ you'll need to have Rails, RubyGems, and MySQL installed.</p>
129
+
130
+ <p>Generate a rails program called <i>test</i>, and change to its
131
+ directory:</p>
132
+
133
+ <blockquote>
134
+ <pre>
135
+ rails test
136
+ cd test
137
+ </pre>
138
+ </blockquote>
139
+
140
+ <p>Install the model_security gem:</p>
141
+
142
+ <blockquote>
143
+ <pre>
144
+ gem install model_security_generator
145
+
146
+ </pre>
147
+ </blockquote>
148
+
149
+ <p>Generate model_security for your application</p>
150
+
151
+ <blockquote>
152
+ <pre>
153
+ script/generate model_security -
154
+
155
+ </pre>
156
+ </blockquote>
157
+
158
+ <p>Have MySQL execute the script to create the database and a
159
+ MySQL user ID for the rails program:</p>
160
+
161
+ <blockquote>
162
+ <pre>
163
+ cd db/
164
+ mysql -u <i>MYSQL-ADMIN-NAME</i> -p &lt; demo.sql
165
+ </pre>
166
+ </blockquote>
167
+
168
+ <p>Examine demo.sql and users.sql . users.sql defines the table
169
+ of users. An accessory table defined by user_configurations.sql
170
+ is used to hold configuration choices for the user system. When
171
+ you include ModelSecurity in another Rails program, you'll skip
172
+ demo.sql and use users.sql and user_configurations.sql to create
173
+ their tables.</p>
174
+
175
+ <p>Edit the database configuration in db/database.yml to look
176
+ like this:</p>
177
+
178
+ <blockquote>
179
+ <pre>
180
+ development:
181
+ adapter: mysql
182
+ database: model_security_demo
183
+ host: localhost
184
+ username: m_s_demo
185
+ password:
186
+ </pre>
187
+ </blockquote>
188
+
189
+ <p>Add some necessary facilities to your brand new application
190
+ controller:</p>
191
+
192
+ <blockquote>
193
+ <pre>
194
+ cd ../app/controllers
195
+ cp ADD_TO_APPLICATION_CONTROLLER.ModelSecurity application.rb
196
+ cd ../..
197
+ </pre>
198
+ </blockquote>
199
+
200
+ <p>If this wasn't a new controller, you would edit application.rb
201
+ to add the facilities requested, rather than copying over the
202
+ file.</p>
203
+
204
+ <p>Start the rails server:</p>
205
+
206
+ <blockquote>
207
+ <pre>
208
+ script/server &amp;
209
+ </pre>
210
+ </blockquote>
211
+
212
+ <p>Open a web browser to http://localhost:3000/user/new Use the
213
+ form to create a new user. That user will automatically be
214
+ granted the administrator role, and will be activated
215
+ immediately.</p>
216
+
217
+ <p>Subsequent users will <i>not</i> automatically get the
218
+ administrator role, although an administrator can grant it to
219
+ them by editing their record with /user/edit/<i>user-name</i> .
220
+ Subsequent users will not have their login activated immediately
221
+ when they create it. Instead, they will be sent an email
222
+ containing a URL that they must use to activate their login.</p>
223
+
224
+ <p>Now, you can play with the demo. The user controller responds
225
+ to:</p>
226
+
227
+ <dl>
228
+ <dt>
229
+ <code>/user/activate/<i>id</i>?token=<i>security-token</i></code></dt>
230
+
231
+ <dd>Activate a user, after the user's login has been
232
+ created.</dd>
233
+
234
+ <dt><code>/user/configure/<i>id</i></code></dt>
235
+
236
+ <dd>
237
+ Configure the User system. Currently allows you to choose:
238
+
239
+ <ul>
240
+ <li>Whether new users must go through email confirmation
241
+ before they can log in.</li>
242
+
243
+ <li>What the sender address is for emails generated by this
244
+ software.</li>
245
+ </ul>The configure function requires the administrator role.
246
+ </dd>
247
+
248
+ <dt><code>/user/destroy/<i>id</i></code></dt>
249
+
250
+ <dd>Destroy a user. Requires the administrator role.</dd>
251
+
252
+ <dt><code>/user/edit/<i>id</i></code></dt>
253
+
254
+ <dd>Edit the attributes of a user. Administrators are allowed
255
+ to edit more fields than normal users, and normal are allowed
256
+ to edit their own record, not anyone else's.</dd>
257
+
258
+ <dt><code>/user/forgot_password</code></dt>
259
+
260
+ <dd>Send an email to the user that facilitates password
261
+ recovery.</dd>
262
+
263
+ <dt><code>/user/list</code></dt>
264
+
265
+ <dd>List the users. Administrators can see more information
266
+ than normal users. Normal users can see some information on
267
+ their own record that they will not see in the records of other
268
+ users.</dd>
269
+
270
+ <dt><code>/user/login</code></dt>
271
+
272
+ <dd>Perform HTTP authentication to log in a user. If that
273
+ doesn't work, fall back on a login form.</dd>
274
+
275
+ <dt><code>/user/logout</code></dt>
276
+
277
+ <dd>Log a user out. Actually loops back to the login action,
278
+ because the only way to get the browser to stop sending HTTP
279
+ authentication data with every request is to ask it to get new
280
+ authentication data from the user.</dd>
281
+
282
+ <dt><code>/user/new</code></dt>
283
+
284
+ <dd>Create a new user.</dd>
285
+
286
+ <dt><code>/user/show</code></dt>
287
+
288
+ <dd>Display information about a user. Administrators can see
289
+ more information than normal users. Normal users can see some
290
+ information on their own record that they will not see in the
291
+ records of other users.</dd>
292
+ </dl>
293
+
294
+ <p>Now that you have the software running, create a user using
295
+ <code>/user/new</code> The first user that you create will
296
+ automatically be granted the administrative privilege, sort of
297
+ like "super-user" status on a Unix or Linux system, and will be
298
+ logged in immediately.</p>
299
+
300
+ <p>You <i>should</i> be able to grant administrative status to
301
+ subsequent users by editing their record and setting the
302
+ <i>admin</i> field to 1, due to a Rails bug (at this writing,
303
+ October 2005), the scaffold views aren't capable of editing
304
+ boolean database fields. We won't need that facility to go
305
+ through our demo.</p>
306
+
307
+ <p>Log out of the application via the /user/logout action. Your
308
+ browser will present a login panel using HTTP authentication. Use
309
+ the <i>cancel</i> button or whatever mechanism your browser
310
+ provides to get rid of that panel. You should now see a login
311
+ form in an HTML page. Don't log in.</p>
312
+
313
+ <p>Run the <code>/user/list</code> action. This action requies a
314
+ login before it will complete. The browser will present the login
315
+ panel again. Log in to the application as your administrative
316
+ user. You'll now see the list of users.</p>
317
+
318
+ <h2>Learning About Login Management And The User Object</h2>
319
+
320
+ <p>Let's look at the code that made that login panel happen. In
321
+ app/controllers/user_controller.rb, you will find this code:</p>
322
+
323
+ <blockquote>
324
+ <pre>
325
+ # Require_login will require a login before the action can be completed.
326
+ # It uses Modal, so it will continue to the action if the login is
327
+ # successful.
328
+ before_filter :require_login, :only =&gt; [ :list, :show ]
329
+
330
+ </pre>
331
+ </blockquote>
332
+
333
+ <p>That code causes require_login to be called before the
334
+ <code>list</code> or <code>show</code> actions. Require_login
335
+ will keep trying to get a user logged in until the login succeeds
336
+ or the user bails out of the process using the back button. If
337
+ you don't yet understand how <i>before-filters</i> work, you'll
338
+ need to peruse the Rails documentation. If a user is already
339
+ logged in, require_login returns <code>true</code> and execution
340
+ proceeds. If no user is logged in, <code>require_login</code>
341
+ will save the current action and then redirect the browser to the
342
+ <code>/usr/login</code> action. Once the user is logged in
343
+ successfully, the browser will be redirected to the previous
344
+ action.</p>
345
+
346
+ <p>Although you might believe that all of this is taking place
347
+ sequentially in your Rails application, that's not the case. It's
348
+ achieved by sending a redirect to the browser as each step
349
+ completes. At each step the browser requests the URL it's been
350
+ redirected to, and Rails carries out an individual action.
351
+ Execution proceeds this way if the user asks for
352
+ <code>/user/list</code> while not logged in:</p>
353
+
354
+ <ol>
355
+ <li>The user asks for <code>/user/list</code></li>
356
+
357
+ <li>Rails redirects the browser to
358
+ <code>/user/login</code></li>
359
+
360
+ <li>The browser asks for <code>/user/login</code></li>
361
+
362
+ <li>Rails sets the HTTP header to request HTTP authentication
363
+ and sends a login form.</li>
364
+
365
+ <li>The browser sends login information.</li>
366
+
367
+ <li>Rails redirects the browser to <code>/user/list</code></li>
368
+
369
+ <li>The browser requests <code>/user/list</code></li>
370
+
371
+ <li>Rails finally sends the list of users.</li>
372
+ </ol>Fortunately, all of this complication is handled for you.
373
+ All you need to know is that you should set
374
+ <code>require_login</code> as a before-filter for any action that
375
+ should only be executed for logged-in users.
376
+
377
+ <p>There's also a <code>require_admin</code> filter. This one is
378
+ used with the <code>destroy</code> action of the user controller,
379
+ as shown by this code:</p>
380
+
381
+ <blockquote>
382
+ <pre>
383
+ # Require_admin will require an administrative login before the action
384
+ # can be called. It uses Modal, so it will continue to the action if the
385
+ # login is successful.
386
+ before_filter :require_admin, :only =&gt; [ :destroy ]
387
+
388
+ </pre>
389
+ </blockquote>
390
+
391
+ <p>That will prevent the <code>destroy</code> action from being
392
+ executed unless the user successfully logs in as an
393
+ administrator.</p>
394
+
395
+ <p><code>require_admin</code> and <code>require_login</code> are
396
+ part of your controller-based security setup. As the first line
397
+ of defense, they will be where potential security violations
398
+ <i>should</i> stop.</p>
399
+
400
+ <p>There's only a little more to learn about logging in and
401
+ users. Take a look at the file
402
+ <code>app/controllers/application.rb</code> . You'll see these
403
+ lines:</p>
404
+
405
+ <blockquote>
406
+ <code>require 'user_support'<br />
407
+ <br />
408
+ class ApplicationController &lt;
409
+ ActionController::Base<br /></code>
410
+
411
+ <blockquote>
412
+ <code>helper :ModelSecurity</code><br />
413
+ <b><code>include UserSupport</code></b><br />
414
+ <b><code>before_filter :user_setup</code></b>
415
+ </blockquote><code>end</code>
416
+ </blockquote>
417
+
418
+ <p>Requiring the file <code>user_support</code> and including the
419
+ module <code>UserSupport</code> mix the login and user-management
420
+ capabilities into the application controller's class. The
421
+ before-filter <code>user_setup</code> handles session control and
422
+ login management for the entire application. Once
423
+ <code>user_setup</code> has run, the <code>User</code> object for
424
+ the currently-logged-in user can be accessed through
425
+ <code>User.current</code> .</p>
426
+
427
+ <h2>Introducing ModelSecurity</h2>
428
+
429
+ <p>That's all you need to know to use the <code>User</code>
430
+ system to manage logins. But we'll keep looking at the
431
+ <code>User</code> code to see how it secures its own data using
432
+ <code>ModelSecurity</code>.</p>
433
+
434
+ <p>Create a second user with <code>/user/new</code> . This user
435
+ won't be privileged. The system will send you an email with
436
+ instructions on how to confirm that user's login. Just in case
437
+ email delivery isn't working on your server, you can get the
438
+ email text out of the log file <code>logs/development.log</code>
439
+ . Confirm the user's login.</p>
440
+
441
+ <p>Logged in as your administrative user, visit
442
+ <code>/user/list</code> . That will display a list of the users
443
+ you've created so far. Because you are the administrator, their
444
+ emails will be visible to you. Now, log out using
445
+ <code>/user/logout</code> and log back in as the non-privileged
446
+ user you created. Visit <code>/user/list</code> again. Now,
447
+ you'll be able to see your own email, but not that of other
448
+ users.</p>
449
+
450
+ <p>Let's look at the code that makes that happen. Edit
451
+ <code>app/models/user.rb</code> and find these lines:</p>
452
+
453
+ <blockquote>
454
+ <pre>
455
+ # If this is a new (never saved) record, or if this record corresponds to
456
+ # the currently-logged-in user, allow reading of the email address.
457
+ let_read :email, :if =&gt; :new_or_me_or_logging_in?
458
+
459
+ </pre>
460
+ </blockquote>
461
+
462
+ <p>This code, in the declaration of the User class, says that the
463
+ <code>email</code> attribute of the model can be read if the
464
+ function <code>new_or_me_or_logging_in?</code> returns true.
465
+ Let's look at that function:</p>
466
+
467
+ <blockquote>
468
+ <pre>
469
+ # Return true if the user record is new (never been saved) or if it
470
+ # corresponds to the currently-logged-in user, or if the current user
471
+ # is the special "login" user. This security test is a common pattern
472
+ # applied to a number of user record attributes.
473
+ def new_or_me_or_logging_in?
474
+ new_record? or User.current == self or logging_in?
475
+ end
476
+
477
+ </pre>
478
+ </blockquote>
479
+
480
+ <p>The first condition is the Rails function
481
+ <code>new_record?</code> which would return true if the record
482
+ has never been saved. The second condition would return true if
483
+ the record refers to the currently-logged-in user, and this is
484
+ what allows the user to see his own email address. The third
485
+ condition returns true if there is currently no logged-in user
486
+ (it contains the expression <code>User.current == nil</code>).
487
+ These are all of the cases in which the application should allow
488
+ reading of the <code>email</code> attribute of a
489
+ <code>User</code> record, <i>except one.</i></p>
490
+
491
+ <p>NOTE: at this time, there is a app/views/user/list.rhtml
492
+ template installed by the generator that overrides the scaffold.
493
+ The difference between this file and the scaffold template is
494
+ that ActiveRecord#readable? is used to determine if a datum
495
+ should be displayed or not. This file will move to
496
+ app/views/scaffolds/list.rhtml once I figure out how to override
497
+ the scaffold template location. - Bruce</p>
498
+
499
+ <p>What tells the software that the administrator can read
500
+ everyone's email address? That's another line in the
501
+ <code>User</code> model declaration:</p>
502
+
503
+ <blockquote>
504
+ <pre>
505
+ # Let the administrator access all data. This implements a Unix-like
506
+ # super-user. Note that the coarse-grained override of the super-user
507
+ # is not a _necessary_ pattern for the ModelSecurity module, you can
508
+ # implement controls as fine-grained as you like.
509
+ let_access :all, :if =&gt; :admin?
510
+
511
+ </pre>
512
+ </blockquote>
513
+
514
+ <p><code>let_access</code> calls <code>let_read</code> and
515
+ <code>let_write</code> with the same arguments, and thus sets
516
+ both read and write permissions. <code>:all</code> is a special
517
+ name that means the permission applies to all of the attributes
518
+ of the data model, not just one database field. And
519
+ <code>admin?</code> is a function that returns true if the
520
+ currently-logged-in user has the administrative privilege. It
521
+ just tests the value of a model attribute called
522
+ <code>admin</code>. The effect of all of this is that the
523
+ administrative user can read or write any datum.</p>
524
+
525
+ <p>You must start by setting the permissions for
526
+ <code>:all</code> in your model if you are using ModelSecurity,
527
+ as the default is to permit all accesses. If you won't be setting
528
+ up an administrative user, as above, it would be a good idea to
529
+ make the default "no access". You can do that with the following
530
+ code:</p>
531
+
532
+ <blockquote>
533
+ <pre>
534
+ # Set the default to "no access".
535
+ let_access :all, :if =&gt; :never?
536
+
537
+ </pre>
538
+ </blockquote>
539
+
540
+ <p>The <code>never?</code> test is provided with ModelSecurity,
541
+ and always returns <code>false</code>. A companion test
542
+ <code>always?</code> reliably returns <code>true</code>.</p>
543
+
544
+ <p>ModelSecurity has two mechanisms for protecting data. One
545
+ protects the actual data model and will throw a exception upon an
546
+ unpermitted access. But that's not what we've been seeing: our
547
+ views have quietly refused to display some unpermitted data,
548
+ without any exceptions. A second mechanism prevents the common
549
+ view functions, including those used by templates, from
550
+ displaying or editing data inappropriately. You activate that
551
+ mechanism by declaring <code>ModelSecurityHelper</code> in your
552
+ application, as below in
553
+ <code>app/controllers/application.rb</code>:</p>
554
+
555
+ <blockquote>
556
+ <code>require 'user_support'<br />
557
+ <br />
558
+ class ApplicationController &lt;
559
+ ActionController::Base<br /></code>
560
+
561
+ <blockquote>
562
+ <b><code>helper :ModelSecurity</code></b><br />
563
+ <code>include UserSupport</code><br />
564
+ <br />
565
+ <code>before_filter :user_setup</code>
566
+ </blockquote><code>end</code>
567
+ </blockquote>
568
+
569
+ <p>This makes the code in <code>ModelSecurityHelper</code>
570
+ available to all views in the application. ModelSecurityHelper
571
+ redefines these common view functions to respect the data model
572
+ permissions:</p>
573
+
574
+ <ul style="list-style-type: none">
575
+ <li>check_box</li>
576
+
577
+ <li>file_field</li>
578
+
579
+ <li>hidden_field</li>
580
+
581
+ <li>password_field</li>
582
+
583
+ <li>radio_button</li>
584
+
585
+ <li>text_area</li>
586
+
587
+ <li>text_field</li>
588
+ </ul>Views that use those functions exclusively to display and
589
+ edit database fields will always refrain from making
590
+ inappropriate read or write accesses.
591
+
592
+ <p>One complication of the view protection is that instance
593
+ variables of your model that are not related to database fields
594
+ are effected in the same way as model attributes. So, when you
595
+ declare the accessors for those instance variables with code like
596
+ this:</p>
597
+
598
+ <blockquote>
599
+ <pre>
600
+ attr_accessible :password, :password_confirmation, :old_password
601
+ </pre>
602
+ </blockquote>
603
+
604
+ <p>You should also declare security permissions on those same
605
+ variables:</p>
606
+
607
+ <blockquote>
608
+ <pre>
609
+ # NOTE: :password, :password_confirmation, and :old_password
610
+ # are not attributes of the record, they are instance variables of the
611
+ # class and aren't written to disk under those names. But I declare them
612
+ # here because otherwise ModelSecurityHelper (which doesn't know that)
613
+ # isn't going to allow me to enter them into a form field.
614
+ #
615
+ # I like how fine-grained I can get.
616
+ let_write :password, :if =&gt; :new_or_me_or_logging_in?
617
+ let_write :password_confirmation, :if =&gt; :new_or_me?
618
+ let_write :old_password, :if =&gt; :me?
619
+ </pre>
620
+ </blockquote>
621
+
622
+ <h2>Display Control</h2>
623
+
624
+ <p>You may also have noticed that there are more fields in the
625
+ database, and more attributes in the User object, than the view
626
+ displays when you visit <code>/user/list</code> . But a quick
627
+ perusal of the code will show that it's a scaffold view. You
628
+ would have expected a scaffold to display all fields. But
629
+ ModelSecurity allows us to override that with the
630
+ <code>let_display</code> declaration. Take a look at its use in
631
+ the <code>User</code> model declaration:</p>
632
+
633
+ <blockquote>
634
+ <pre>
635
+ # These just control display.
636
+ let_display :all, :if =&gt; :never? # Set default to don't display, override below.
637
+ let_display :admin, :activated, :if =&gt; :admin?
638
+ let_display :login, :name, :email
639
+ </pre>
640
+ </blockquote>
641
+
642
+ <p>This declaration will cause the scaffold view to present
643
+ information about three fields, <code>login</code>,
644
+ <code>name</code> and <code>email</code> to all users (although a
645
+ <code>let_read</code> declaration further restricts whose email
646
+ address a non-privileged user can see, as above). Administrative
647
+ users will also see the fields <code>admin</code> and
648
+ <code>activated</code>. Nobody will see the fields
649
+ <code>cypher</code> and <code>salt</code>, they are long strings
650
+ of encrypted or random cybercrud that nobody wants to read. Note
651
+ that the scaffold view will format the display as a table with
652
+ columns only for the data that it will display. All of this is
653
+ achieved by overloading the <code>content_columns</code> method
654
+ of <code>ActiveRecord</code> to filter the columns it reports
655
+ using the data in the <code>let_display</code> declaration.</p>
656
+
657
+ <p>While the tests used with <code>let_read</code>,
658
+ <code>let_write</code>, and <code>let_access</code> are instance
659
+ methods of your <code>ActiveRecord</code> class, tests used with
660
+ <code>let_display</code> must be class methods. This is because
661
+ the decision of what columns to display is made before the first
662
+ row's instance is accessed. Rails makes it easy enough to declare
663
+ a method to work with both an instance and its class.</p>
664
+
665
+ <p>You've now completed the ModelSecurity tutorial. For more
666
+ information, see the <a href=
667
+ "http://perens.com/FreeSoftware/ModelSecurity/Reference/">reference</a>.</p>
668
+ </body>
669
+ </html>
@@ -20,7 +20,7 @@ private
20
20
  # Require_admin will require an administrative login before the action
21
21
  # can be called. It uses Modal, so it will continue to the action if the
22
22
  # login is successful.
23
- before_filter :require_admin, :only => [ :destroy ]
23
+ before_filter :require_admin, :only => [ :configure, :destroy ]
24
24
 
25
25
  # Require_login will require a login before the action
26
26
  # can be called. It uses Modal, so it will continue to the action if the
@@ -36,6 +36,26 @@ private
36
36
  render :status => 401
37
37
  end
38
38
 
39
+ # Promote the first user to be an administrator, and don't make that user
40
+ # confirm.
41
+ def promote
42
+ @user.activated = 1
43
+ @user.admin = 1
44
+ @user.save
45
+ end
46
+
47
+ # Send an email to a user with details on how to confirm his login.
48
+ def send_confirmation_email(params)
49
+ url_params = {
50
+ :controller => 'user',
51
+ :action => 'activate',
52
+ :id => @user.id,
53
+ :token => @user.new_token
54
+ }
55
+ url = url_for(url_params)
56
+ UserMailer.deliver_new_user(params, url, @user.token_expiry)
57
+ end
58
+
39
59
  public
40
60
 
41
61
  # Activate a new user, having logged in with a security token. All of the
@@ -43,6 +63,102 @@ public
43
63
  def activate
44
64
  end
45
65
 
66
+ before_filter :require_admin, :only => [ :configure, :destroy ]
67
+
68
+ # Require_login will require a login before the action
69
+ # can be called. It uses Modal, so it will continue to the action if the
70
+ # login is successful.
71
+ before_filter :require_login, :only => [ :list, :show ]
72
+
73
+ # Cause HTTP Basic validation.
74
+ # FIX: Basic is not secure. Change to Digest authentication.
75
+ def http_authorize(realm=@request.domain(2))
76
+ # This will cause the browser to send HTTP authorization information.
77
+ @response.headers["Status"] = "Unauthorized"
78
+ @response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
79
+ render :status => 401
80
+ end
81
+
82
+ # Activate a new user, having logged in with a security token. All of the
83
+ # work goes on in user_support.
84
+ def activate
85
+ end
86
+
87
+ # Send out a forgot-password email.
88
+ def forgot_password
89
+ case @request.method
90
+ when :get
91
+ @user = User.new
92
+ when :post
93
+ @user = User.find_first(['email = ?', @params['user']['email']])
94
+ if @user
95
+ url = url_for(:controller => 'user', :action => 'activate', :id => @user.id, :token => @user.new_token)
96
+ UserMailer.deliver_forgot_password(@user, url)
97
+ render :action => 'forgot_password_done'
98
+ else
99
+ flash['notice'] = "Can't find a user with email #{@params['email']}."
100
+ @user = User.new
101
+ end
102
+ end
103
+ end
104
+
105
+ # Tell the user the email's on the way.
106
+ def forgot_password_done
107
+ end
108
+
109
+ def list
110
+ @users = User.find_all
111
+ end
112
+
113
+ # Attempt HTTP authentication, and fall back on a login form.
114
+ # If this method is called login_admin (it's an alias), keep trying
115
+ # until an administrator logs in or the user pushes the "back" button.
116
+ def login
117
+ if flash[:login_succeeded]
118
+ redirect_back_or_default :action => :success
119
+ return
120
+ end
121
+
122
+ @user = User.new
123
+
124
+ flash[:login_succeeded] = false
125
+ http_authorize
126
+ end
127
+
128
+ # Log in an administrator. If a non-administrator logs in, keep trying
129
+ # until an administrator logs in or the user pushes the "back" button.
130
+ alias login_admin login
131
+
132
+ # Log out the current user, attempt HTTP authentication to log in a new
133
+ # user. The session information skip_user_setup=true tells the server to
134
+ # generate a new HTTP authentication request and ignore the current HTTP
135
+ # authentication data.
136
+ # We have to request new HTTP authentication here to make the browser
137
+ # forget the old authentication data. Otherwise, the browser keeps sending
138
+ # it!
139
+ def logout
140
+ User.sign_off
141
+ reset_session
142
+ flash[:skip_user_setup] = true
143
+ redirect_to :action => 'login'
144
+ end
145
+
146
+ # Configure this system.
147
+ def configure
148
+ case @request.method
149
+ when :get
150
+ @user_configuration = UserConfiguration.get
151
+ when :post
152
+ c = (@user_configuration = UserConfiguration.get)
153
+ c.attributes = @params['user_configuration']
154
+ if c.save
155
+ flash['notice'] = "Configuration saved."
156
+ else
157
+ flash['notice'] = "Configuration NOT saved."
158
+ end
159
+ end
160
+ end
161
+
46
162
  # Send out a forgot-password email.
47
163
  def forgot_password
48
164
  case @request.method
@@ -106,27 +222,27 @@ public
106
222
  when :post
107
223
  p = @params['user']
108
224
  @user = User.new(p)
225
+
226
+ if UserConfiguration.get.email_confirmation == 0
227
+ @user.activated = 1
228
+ end
229
+
109
230
  if @user.save
110
231
  flash['notice'] = 'User created.'
111
232
  # Promote the very first user to be an administrator.
112
233
  if @user.id == 1
113
- @user.admin = 1
114
- @user.activated = 1
115
- @user.save
234
+ promote
116
235
  User.current = @user
117
236
  render :action => 'admin_created'
118
- # Mail the user instructions on how to activate their account.
119
237
  else
120
- url_params = {
121
- :controller => 'user',
122
- :action => 'activate',
123
- :id => @user.id,
124
- :token => @user.new_token
125
- }
126
- url = url_for(url_params)
127
- UserMailer.deliver_new_user(p, url, @user.token_expiry)
128
- @email = p['email']
129
- render :action => 'created'
238
+ # Mail the user instructions on how to activate their account.
239
+ if UserConfiguration.get.email_confirmation == 1
240
+ send_confirmation_email(p)
241
+ @email = p['email']
242
+ render :action => 'created'
243
+ else
244
+ render :action => 'success'
245
+ end
130
246
  end
131
247
  else
132
248
  flash['notice'] = 'Creation of new user failed.'
@@ -1,4 +1,4 @@
1
1
  create database model_security_demo;
2
2
  use model_security_demo;
3
- source users.sql;
3
+ source schema.sql;
4
4
  grant all on model_security_demo.* to 'm_s_demo'@'localhost';
@@ -0,0 +1,2 @@
1
+ source users.sql;
2
+ source user_configurations.sql;
@@ -0,0 +1,7 @@
1
+ create table user_configurations (
2
+ id integer unsigned not null auto_increment primary key,
3
+ email_confirmation tinyint unsigned not null default 1,
4
+ email_sender text not null,
5
+ created_on timestamp not null,
6
+ updated_on timestamp not null
7
+ );
@@ -5,7 +5,7 @@ end
5
5
 
6
6
  module ActiveRecord
7
7
  class Base
8
- once('@instanceAliasesDone') {
8
+ once('@modelSecurityInstanceAliasesDone') {
9
9
  # Provides access to the pre-overload version of read_attribute, which
10
10
  # is called by the overloaded version.
11
11
  alias :old_read_attribute :read_attribute
@@ -15,9 +15,17 @@ module ActiveRecord
15
15
  alias :old_write_attribute :write_attribute
16
16
  }
17
17
 
18
+ def readable?(name)
19
+ true
20
+ end
21
+
22
+ def writable?(name)
23
+ true
24
+ end
25
+
18
26
  class << self
19
27
  private
20
- once('@classAliasesDone') {
28
+ once('@modelSecurityClassAliasesDone') {
21
29
  # Provides access to the pre-overload version of content_columns, which
22
30
  # is called by the overloaded version.
23
31
  alias :old_content_columns :content_columns
@@ -32,6 +40,11 @@ module ActiveRecord
32
40
  not display?(c.name)
33
41
  }
34
42
  end
43
+
44
+ # Always return true. ModelSecurity.display? will override this.
45
+ def display?(name)
46
+ true
47
+ end
35
48
  end
36
49
  end
37
50
  end
@@ -96,7 +109,7 @@ module ActionView
96
109
 
97
110
  # Evaluating the aliases twice would break them.
98
111
  # Evaluating the constant definition twice causes a complaint.
99
- once ('@aliasesDone') {
112
+ once ('@modelSecurityAliasesDone') {
100
113
  alias :old_check_box :check_box
101
114
  alias :old_radio_button :radio_button
102
115
  }
@@ -32,12 +32,15 @@ module Modal
32
32
  if r = @params['ret']
33
33
  logger.info("modal_setup: Save location #{r.sub(/\.L/, '#L')}")
34
34
  self.return_location = r.sub(/\.L/, '#L')
35
- elsif return_location.nil?
36
- r = @request.env['HTTP_REFERER']
37
- if r
38
- logger.info("modal_setup: Save HTTP Referer #{r}")
39
- self.return_location = @request.env['HTTP_REFERER']
40
- end
35
+ # This results in redirection loops, as currently written.
36
+ # It needs a hueristic to carefully avoid them before we try it again.
37
+ #
38
+ # elsif return_location.nil?
39
+ # r = @request.env['HTTP_REFERER']
40
+ # if r
41
+ # logger.info("modal_setup: Save HTTP Referer #{r}")
42
+ # self.return_location = @request.env['HTTP_REFERER']
43
+ # end
41
44
  end
42
45
  true
43
46
  end
@@ -0,0 +1,18 @@
1
+ class UserConfiguration < ActiveRecord::Base
2
+ public
3
+ attr_accessible :email_confirmation, :email_sender
4
+
5
+ def self.get
6
+ begin
7
+ u = self.find(1)
8
+ rescue
9
+ c = self.new
10
+ end
11
+ end
12
+
13
+ def initialize(parameters = nil)
14
+ super
15
+ self.email_confirmation = 1
16
+ self.email_sender = ''
17
+ end
18
+ end
@@ -2,7 +2,15 @@ ActionMailer::Base.delivery_method = :sendmail
2
2
 
3
3
  # Send mail to a user to administer that user's login. Called by UserController.
4
4
  class UserMailer < ActionMailer::Base
5
+ private
6
+ def set_sender
7
+ s = UserConfiguration.get.email_sender
8
+ if s.length > 0
9
+ from s
10
+ end
11
+ end
5
12
 
13
+ public
6
14
  # Send a forgot-password email, allowing the user to regain their login name
7
15
  # and password.
8
16
  def forgot_password(user, url)
@@ -11,8 +19,7 @@ class UserMailer < ActionMailer::Base
11
19
 
12
20
  recipients user.email
13
21
  subject 'Login and password recovery.'
14
- from 'bruce@perens.com'
15
-
22
+ set_sender
16
23
  end
17
24
 
18
25
  # Send a new-user email, providing the user with a URL used to validate
@@ -24,7 +31,6 @@ class UserMailer < ActionMailer::Base
24
31
 
25
32
  recipients params['email']
26
33
  subject 'Your new login is ready'
27
- from 'bruce@perens.com'
28
-
34
+ set_sender
29
35
  end
30
36
  end
@@ -0,0 +1,26 @@
1
+ <h1>Configure the User System</h1>
2
+
3
+ <% if flash['notice'] %>
4
+ <p><%= flash['notice'] %></p>
5
+ <% end %>
6
+
7
+ <%= error_messages_for 'user_configuration' %>
8
+ <%= form_tag :action => 'configure' %>
9
+ <table>
10
+ <tr>
11
+ <th><label for="user_configuration_email_confirmation">
12
+ Email confirmation:
13
+ </label></th>
14
+ <td>
15
+ <%= check_box 'user_configuration', 'email_confirmation' %>
16
+ </td>
17
+ </tr>
18
+ <tr>
19
+ <th><label for="user_configuration_email_sender">
20
+ Email Sender:
21
+ </label></th>
22
+ <td><%= text_field 'user_configuration', 'email_sender' %></td>
23
+ </tr>
24
+ </table>
25
+ <%= submit_tag "Save" %>
26
+ <%= end_form_tag %>
@@ -0,0 +1,28 @@
1
+ <h1>Listing <%= @scaffold_plural_name %></h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <% for column in User.content_columns %>
6
+ <th><%= column.human_name %></th>
7
+ <% end %>
8
+ </tr>
9
+
10
+ <% for entry in @users %>
11
+ <tr>
12
+ <% for column in User.content_columns %>
13
+ <td>
14
+ <% if entry.readable?(column.name) %>
15
+ <%= entry.send(column.name) %>
16
+ <% end %>
17
+ </td>
18
+ <% end %>
19
+ <td><%= link_to "Show", :action => "show", :id => entry %></td>
20
+ <td><%= link_to "Edit", :action => "edit", :id => entry %></td>
21
+ <td><%= link_to "Destroy", {:action => "destroy", :id => entry}, {:confirm => "Are you sure?"} %></td>
22
+ </tr>
23
+ <% end %>
24
+ </table>
25
+
26
+ <br />
27
+
28
+ <%= link_to "New User", :action => "new" %>
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.4
3
3
  specification_version: 1
4
4
  name: model_security_generator
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.7
7
- date: 2005-10-11
6
+ version: 0.0.8
7
+ date: 2005-10-12
8
8
  summary: "[Rails] Model security and authentication generator."
9
9
  require_paths:
10
10
  - "."
@@ -32,10 +32,13 @@ files:
32
32
  - templates/README
33
33
  - templates/USAGE
34
34
  - templates/RUN_DEMO
35
+ - templates/Tutorial.html
35
36
  - templates/ADD_TO_APPLICATION_CONTROLLER
36
37
  - templates/controllers/user_controller.rb
37
38
  - templates/db/demo.sql
39
+ - templates/db/schema.sql
38
40
  - templates/db/users.sql
41
+ - templates/db/user_configurations.sql
39
42
  - templates/helpers/modal_helper.rb
40
43
  - templates/helpers/model_security_helper.rb
41
44
  - templates/lib/modal.rb
@@ -45,6 +48,7 @@ files:
45
48
  - templates/mailer/forgot_password.rhtml
46
49
  - templates/mailer/new_user.rhtml
47
50
  - templates/models/user.rb
51
+ - templates/models/user_configuration.rb
48
52
  - templates/models/user_mailer.rb
49
53
  - templates/test/mock_mailer.rb
50
54
  - templates/test/mock_time.rb
@@ -53,11 +57,12 @@ files:
53
57
  - templates/test/users.yml
54
58
  - templates/views/activate.rhtml
55
59
  - templates/views/admin_created.rhtml
60
+ - templates/views/configure.rhtml
56
61
  - templates/views/created.rhtml
57
62
  - templates/views/forgot_password.rhtml
58
63
  - templates/views/forgot_password_done.rhtml
64
+ - templates/views/list.rhtml
59
65
  - templates/views/login.rhtml
60
- - templates/views/login_admin.rhtml
61
66
  - templates/views/logout.rhtml
62
67
  - templates/views/new.rhtml
63
68
  - templates/views/success.rhtml
@@ -1,16 +0,0 @@
1
- <p>
2
- You must have the administrator role to proceed.<br/>
3
- If you aren't an administrator, please use the "back" button in your browser to exit this action.
4
- </p>
5
- <p>
6
- <b>Administrator login</b>
7
- </p>
8
- <%= start_form_tag %>
9
- <p><label for="user_login">User ID</label><br/>
10
- <%= text_field 'user', 'login' %></p>
11
-
12
- <p><label for="user_password">Password</label><br/>
13
- <%= password_field 'user', 'password' %></p>
14
-
15
- <%= submit_tag 'Login' %>
16
- <%= end_form_tag %>