model_security_generator 0.0.7 → 0.0.8

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