model_security_generator 0.0.1

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.
data/templates/once.rb ADDED
@@ -0,0 +1,36 @@
1
+ # Do something only once. Called this way:
2
+ #
3
+ # once('variable-name') { block }
4
+ #
5
+ # *variable-name* is a string. The corresponding variable is referenced
6
+ # within the block's binding using eval().
7
+ #
8
+ # If the variable doesn't exist, or if it's false or nil:
9
+ # The block is executed.
10
+ # The variable is set to true, within the block's binding.
11
+ # True is returned.
12
+ #
13
+ # If the variable is true:
14
+ #
15
+ # False is returned.
16
+ #
17
+ # Although this will by default create a simple boolean, more complex
18
+ # variables such as object.method calls, array and hash references work
19
+ # fine. I've used it effectively with 2-dimensional hashes.
20
+ #
21
+ # Copyright (C) 2005 Bruce Perens <bruce@perens.com>
22
+ # Distributable under the same license as Ruby.
23
+ def once var, &block
24
+ begin
25
+ result = eval(var, block.binding)
26
+ rescue NameError
27
+ result = false
28
+ end
29
+ if not result
30
+ eval(var + " = true", block.binding)
31
+ yield
32
+ true
33
+ else
34
+ false
35
+ end
36
+ end
@@ -0,0 +1,74 @@
1
+ body { background-color: #fff; color: #333; }
2
+
3
+ body, p, ol, ul, td {
4
+ font-family: verdana, arial, helvetica, sans-serif;
5
+ font-size: 13px;
6
+ line-height: 18px;
7
+ }
8
+
9
+ pre {
10
+ background-color: #eee;
11
+ padding: 10px;
12
+ font-size: 11px;
13
+ }
14
+
15
+ a { color: #000; }
16
+ a:visited { color: #666; }
17
+ a:hover { color: #fff; background-color:#000; }
18
+
19
+ .fieldWithErrors {
20
+ padding: 2px;
21
+ background-color: red;
22
+ display: table;
23
+ }
24
+
25
+ #ErrorExplanation {
26
+ width: 400px;
27
+ border: 2px solid 'red';
28
+ padding: 7px;
29
+ padding-bottom: 12px;
30
+ margin-bottom: 20px;
31
+ background-color: #f0f0f0;
32
+ }
33
+
34
+ #ErrorExplanation h2 {
35
+ text-align: left;
36
+ font-weight: bold;
37
+ padding: 5px 5px 5px 15px;
38
+ font-size: 12px;
39
+ margin: -7px;
40
+ background-color: #c00;
41
+ color: #fff;
42
+ }
43
+
44
+ #ErrorExplanation p {
45
+ color: #333;
46
+ margin-bottom: 0;
47
+ padding: 5px;
48
+ }
49
+
50
+ #ErrorExplanation ul li {
51
+ font-size: 12px;
52
+ list-style: square;
53
+ }
54
+
55
+ div.uploadStatus {
56
+ margin: 5px;
57
+ }
58
+
59
+ div.progressBar {
60
+ margin: 5px;
61
+ }
62
+
63
+ div.progressBar div.border {
64
+ background-color: #fff;
65
+ border: 1px solid grey;
66
+ width: 100%;
67
+ }
68
+
69
+ div.progressBar div.background {
70
+ background-color: #333;
71
+ height: 18px;
72
+ width: 0%;
73
+ }
74
+
@@ -0,0 +1,11 @@
1
+ <html>
2
+ <head>
3
+ <title>: <%= controller.action_name %></title>
4
+ <%= stylesheet_link_tag 'scaffold' %>
5
+ </head>
6
+ <body>
7
+
8
+ <%= @content_for_layout %>
9
+
10
+ </body>
11
+ </html>
@@ -0,0 +1,4 @@
1
+ create database model_security_demo;
2
+ use model_security_demo;
3
+ source users.sql;
4
+ grant all on model_security_demo.* to 'm_s_demo'@'localhost';
@@ -0,0 +1,7 @@
1
+ .flash {
2
+ text-align: center;
3
+ background-color: #C0C0FF;
4
+ border: medium double #FF0000;
5
+ margin: 4px;
6
+ }
7
+
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3
+ <head>
4
+ <link href="/stylesheets/standard.css" rel="stylesheet" type="text/css" />
5
+ </head>
6
+ <body class="page">
7
+ <% if flash['notice'] %>
8
+ <center>
9
+ <div class="flash">
10
+ <%=h flash['notice'] %>
11
+ </div>
12
+ </center>
13
+ <% end %>
14
+ <%= @content_for_layout %>
15
+ </body>
16
+ </html>
data/templates/user.rb ADDED
@@ -0,0 +1,328 @@
1
+ require 'digest/sha2'
2
+ require 'model_security'
3
+
4
+ # A generic user login facility. Provides a user login, password
5
+ # management, and administrative facilities. Logs users in via HTTP Basic
6
+ # authentication, a login form, or a security token. Maintains the login
7
+ # state using Session.
8
+ #
9
+ # I started out with the Salted Hash login generator, and essentially rewrote
10
+ # the whole thing, learning a lot from the previous versions. This is not a
11
+ # criticism of the previous work, my goals were different. So, it's fair to
12
+ # say that this is derived from the work of Joe Hosteny and Tobias Leutke.
13
+ #
14
+ class User < ActiveRecord::Base
15
+ # This causes the security features to be added to the model.
16
+ include ModelSecurity
17
+
18
+ private
19
+ attr_accessible :login, :name, :email, :password, :password_confirmation, \
20
+ :old_password
21
+
22
+ # Hash a given password with the salt. This method localizes the encryption
23
+ # function so that it can be easily changed.
24
+ def encypher(s)
25
+ Digest::SHA512.hexdigest(salt + s)
26
+ end
27
+
28
+ # Create a new user record.
29
+ #
30
+ # This is either used to create an ephemeral prototype object to initialize
31
+ # a form, or an object resulting from a form submission that will become a
32
+ # persistent record.
33
+ #
34
+ def initialize(attributes = nil)
35
+ super
36
+
37
+ if password
38
+ @password_is_new = true
39
+ end
40
+ end
41
+
42
+ # Returns true if it is intended that the password be replaced when this
43
+ # record is saved.
44
+ def password_new?
45
+ @password_is_new
46
+ end
47
+
48
+ Char64 = (('a'..'z').collect + ('A'..'Z').collect + ('0'..'9').collect + ['.','/']).freeze
49
+
50
+ # Create a security token for use in logging in a user who has forgotten
51
+ # his password or has just created his login.
52
+ def token_string(n)
53
+ s = ""
54
+ n.times { s << Char64[(Time.now.tv_usec * rand) % 64] }
55
+ s
56
+ end
57
+
58
+ # Validates that initialize() sets @password_is_new to true, so that
59
+ # password validation works correctly. This would fail only in the case
60
+ # of a programming error.
61
+ def validate_on_create
62
+ password_new?
63
+ end
64
+
65
+ # Validates that if we're changing the password or email, the old password
66
+ # has been given and matches the record. This is a defense against
67
+ # cookie-capture attacks.
68
+ def validate_on_update
69
+ if not (id.nil? or User.admin?) and (password_new? or @email_is_new)
70
+ if encypher(old_password) != cypher
71
+ errors.add(:old_password, "The old password doesn't match.")
72
+ return false
73
+ end
74
+ end
75
+ true
76
+ end
77
+
78
+ before_save :prepare_save
79
+
80
+ validates_presence_of :login
81
+ validates_uniqueness_of :login
82
+ validates_uniqueness_of :email
83
+ validates_format_of :email, \
84
+ :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/
85
+ validates_presence_of :password, :if => :password_new?
86
+ validates_presence_of :password_confirmation, :if => :password_new?
87
+ validates_length_of :login, :within => 3..80
88
+ validates_length_of :password, :within => 5..128, :if => :password_new?
89
+ validates_confirmation_of :password, :if => :password_new?
90
+
91
+ # Here are the new model security specifications.
92
+
93
+ # These just control display.
94
+ let_display :all, :if => :never?
95
+ let_display :login, :name, :email
96
+
97
+ # These control both reading and writing.
98
+
99
+ # Let the administrator access all data. This implements a Unix-like
100
+ # super-user. Note that the coarse-grained override of the super-user
101
+ # is not a _necessary_ pattern for the ModelSecurity module, you can
102
+ # implement controls as fine-grained as you like.
103
+ let_access :all, :if => :admin?
104
+
105
+ # These control reading of model attributes.
106
+
107
+ # The controller before_filter require_admin tests if the current user
108
+ # is the administrator, thus we have to make the admin attribute readable
109
+ # by all. We would not have to make it readable were admin? only being
110
+ # used as a security test, as security tests can access all attributes
111
+ # with impunity. Login and name are public information.
112
+ #
113
+ # FIX: Reading "id" is necessary for Rails internals.
114
+ # Make it readable by default in the ModelSecurity module.
115
+ let_read :admin, :id, :login, :name
116
+
117
+ # If this is a new (never saved) record, or if this record corresponds to
118
+ # the currently-logged-in user, allow reading of the email address,
119
+ # timestamps and lock_version.
120
+ #
121
+ # FIX: Lock_version is read by rails internals when a record is saved,
122
+ # make it readable by default in the ModelSecurity module.
123
+ let_read :created_on, :email, :updated_on, :lock_version, :if => :new_or_me_or_logging_in?
124
+
125
+ # These attributes are concerned with login security, and can only be read
126
+ # while a user is logging in. We create a pseudo-user for the process of
127
+ # logging in and a security test :logging_in? that tests for that user.
128
+ let_read :activated, :cypher, :salt, :token, :token_expiry, \
129
+ :if => :logging_in?
130
+
131
+ # These control writing of model attributes.
132
+
133
+ # Only in the case of a new (never saved) record can these fields be written.
134
+ #
135
+ # FIX: Rails internals writes the ID and created_on. Make this test a
136
+ # default for :id and :created_on within the ModelSecurity module. Document
137
+ # it to the user.
138
+ let_write :id, :created_on, :login, :name, :if => :new_record?
139
+
140
+ # Only allow this information to be updated by the user who owns the record,
141
+ # unless this record is new (has never been saved).
142
+ #
143
+ # FIX: There should be a default write permission for updated_on and
144
+ # lock_version within the ModelSecurity module, as Rails internals
145
+ # write them.
146
+ let_write :cypher, :email, :salt, :if => :new_or_me?
147
+
148
+ # The security token can only be changed if we're the special "login" user.
149
+ let_write :activated, :token, :token_expiry, :if => :logging_in?
150
+
151
+ let_write :updated_on, :lock_version, :if => :new_or_me_or_logging_in?
152
+
153
+ public
154
+ attr_accessor :password, :password_confirmation, :old_password
155
+
156
+ # Change the user's password. Confirm the old password while doing so.
157
+ def change_password(attributes)
158
+ @password_is_new = true
159
+ self.password = attributes['password']
160
+ self.password_confirmation = attributes['password_confirmation']
161
+ self.old_password = attributes['old_password']
162
+ end
163
+
164
+ # Change the user's email address.
165
+ # FIX: send confirmation email.
166
+ def change_email(attributes)
167
+ @email_is_new = true
168
+ self.email = attributes['email']
169
+ self.old_password = attributes['old_password']
170
+ end
171
+
172
+ # Return the currently-logged-in user.
173
+ def User.current
174
+ @current_user
175
+ end
176
+
177
+ # Set the currently-logged-in user.
178
+ def User.current=(u)
179
+ @current_user = u
180
+ end
181
+
182
+ # Return true if this record corresponds to the currently-logged-in user.
183
+ # This is used as a security test.
184
+ def me?
185
+ User.current and User.current.id == id
186
+ end
187
+
188
+ # Return true if the currently-logged-in user is the administrator.
189
+ # Class method. This is used as a pseudo-security test by let_display.
190
+ def User.admin?
191
+ return ((current != nil ) and (current.admin.to_i == 1))
192
+ end
193
+
194
+ # Return true if the currently-logged-in user is the administrator.
195
+ # Instance method. This is used as a security test.
196
+ def admin?
197
+ return User.admin?
198
+ end
199
+
200
+ # Return true if the user is currently logging in. This security test allows
201
+ # us to designate model fields to be visible *only* while a user is logging
202
+ # in.
203
+ def logging_in?
204
+ # FIX: create a real login user.
205
+ return User.current.nil?
206
+ end
207
+
208
+ def User.login_user
209
+ # FIX: create a real login user.
210
+ nil
211
+ end
212
+
213
+ # Return true if the user record is new (never been saved) or if it
214
+ # corresponds to the currently-logged-in user. This security test is
215
+ # a common pattern applied to a number of user record attributes.
216
+ def new_or_me?
217
+ new_record? or User.current == self
218
+ end
219
+
220
+ # Return true if the user record is new (never been saved) or if it
221
+ # corresponds to the currently-logged-in user, or if the current user
222
+ # is the special "login" user. This security test is a common pattern
223
+ # applied to a number of user record attributes.
224
+ def new_or_me_or_logging_in?
225
+ new_record? or User.current == self or logging_in?
226
+ end
227
+
228
+ # Create a new security token, or if the current one is not yet expired,
229
+ # return the current one. Should only be called with nobody logged in, it
230
+ # will log out the current user if one is logged in.
231
+ # Instance method.
232
+ def new_token
233
+ User.current = User.login_user
234
+ if token == '' or token_expiry < Time.now
235
+ self.token = token_string(10)
236
+ self.token_expiry = 7.days.from_now
237
+ result = save
238
+ end
239
+ User.current = nil
240
+ return token
241
+ end
242
+
243
+ # Create a new security token, or if the current one is not yet expired,
244
+ # return the current one. Should only be called with nobody logged in, it
245
+ # will log out the current user if one is logged in.
246
+ # Class method.
247
+ def User.new_token(address)
248
+ u = User.find_first(['email = ?', email])
249
+ u.new_token
250
+ end
251
+
252
+ # Encrypt the password before saving. Then wipe out the provided plaintext
253
+ # password, so that it won't trigger unnecessary security tests and
254
+ # validations the next time this record is saved. Wiping out the plaintext
255
+ # is more secure, anyway.
256
+ def prepare_save
257
+ # The salt is used to add a random factor to the plaintext. This might
258
+ # make some cryptographic attacks more difficult.
259
+ if password_new?
260
+ self.salt = token_string(40)
261
+ self.cypher = encypher(password)
262
+
263
+ self.password = nil
264
+ self.password_confirmation = nil
265
+ @password_is_new = nil
266
+ end
267
+ true
268
+ end
269
+
270
+ # Log off the current user.
271
+ def User.sign_off
272
+ User.current = nil
273
+ end
274
+
275
+ # Log on the user for this record, given a password. Instance method.
276
+ def sign_on(pass)
277
+ User.current = User.login_user
278
+ begin
279
+ if (activated == 1) and (pass != nil) and (encypher(pass) == cypher)
280
+ return (User.current = self)
281
+ end
282
+ rescue
283
+ end
284
+ User.current = nil
285
+ end
286
+
287
+ # Log on the user for this record, given a user name and password.
288
+ # Class method.
289
+ def User.sign_on(handle, pwd)
290
+ user = find_first(['login = ?', handle])
291
+ if user
292
+ user.sign_on(pwd)
293
+ else
294
+ nil
295
+ end
296
+ end
297
+
298
+ # Continue the current login, from the session data.
299
+ # This should be called by User.setup .
300
+ def User.sign_on_by_session(user_id)
301
+ begin
302
+ return (User.current = User.find(user_id))
303
+ rescue
304
+ end
305
+ return nil
306
+ end
307
+
308
+ # Sign on the user using a security token. Instance method.
309
+ def sign_on_by_token(t)
310
+ User.current = User.login_user
311
+ if t == token and (token_expiry >= Time.now)
312
+ self.token = ""
313
+ self.token_expiry = Time.now
314
+ self.activated = 1
315
+ save
316
+ User::current = self
317
+ return self;
318
+ end
319
+ return nil
320
+ end
321
+
322
+ # Sign on the user using an ID (record index) and security token.
323
+ # Class method.
324
+ def User.sign_on_by_token(id, token)
325
+ u = User.find(id)
326
+ return u.sign_on_by_token(token)
327
+ end
328
+ end
@@ -0,0 +1,178 @@
1
+ # The HTTP authorization code is
2
+ # derived from an example published by Maximillian Dornseif at
3
+ # http://blogs.23.nu/c0re/stories/7409/
4
+ # which was released for use under any license.
5
+ #
6
+ # I started out with the Salted Hash login generator, and essentially rewrote
7
+ # the whole thing, learning a lot from the previous versions. This is not a
8
+ # criticism of the previous work, my goals were different. So, it's fair to
9
+ # say that this is derived from the work of Joe Hosteny and Tobias Leutke.
10
+ #
11
+ class UserController < ApplicationController
12
+ # helper ModelSecurity
13
+
14
+ private
15
+ # Require_admin will require an administrative login before the action
16
+ # can be called. It uses Modal, so it will continue to the action if the
17
+ # login is successful.
18
+ before_filter :require_admin, :only => [ :destroy ]
19
+
20
+ # Require_login will require a login before the action
21
+ # can be called. It uses Modal, so it will continue to the action if the
22
+ # login is successful.
23
+ before_filter :require_login, :only => [ :list, :show ]
24
+
25
+ # Cause HTTP Basic validation.
26
+ # FIX: Basic is not secure. Change to Digest authentication.
27
+ def http_authorize(realm=@request.domain(2))
28
+ # This will cause the browser to send HTTP authorization information.
29
+ @response.headers["Status"] = "Unauthorized"
30
+ @response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
31
+ render :status => 401
32
+ end
33
+
34
+ def initialize
35
+ end
36
+
37
+ public
38
+ scaffold :user
39
+
40
+ private
41
+ public
42
+
43
+ # Activate a new user, having logged in with a security token. All of the
44
+ # work goes on in user_support.
45
+ def activate
46
+ end
47
+
48
+ # Destroy a user object.
49
+ def destroy
50
+ user = User.find(@params[:id])
51
+ user.destroy
52
+ flash['notice'] = 'User destroyed.'
53
+ redirect_to :action => :list
54
+ end
55
+
56
+ # Edit a user. Will only allow you to edit what your security clearance
57
+ # allows, due to the magic of model security.
58
+ def edit
59
+ case @request.method
60
+ when :get
61
+ @user = User.find(@params[:id])
62
+ when :post
63
+ @user = User.find(@params[:id])
64
+ @user.attributes = @params['user']
65
+ @user.save
66
+ redirect_to :action => :list
67
+ end
68
+ end
69
+
70
+ # Send out a forgot-password email.
71
+ def forgot_password
72
+ case @request.method
73
+ when :get
74
+ @user = User.new
75
+ when :post
76
+ @user = User.find_first(['email = ?', @params['user']['email']])
77
+ if @user
78
+ url = url_for(:controller => 'user', :action => 'activate', :id => @user.id, :token => @user.new_token)
79
+ UserMailer.deliver_forgot_password(@user, url)
80
+ render :action => 'forgot_password_done'
81
+ else
82
+ flash['notice'] = "Can't find a user with email #{@params['email']}."
83
+ @user = User.new
84
+ end
85
+ end
86
+ end
87
+
88
+ # Tell the user the email's on the way.
89
+ def forgot_password_done
90
+ end
91
+
92
+ # List users.
93
+ # FIX: Get away from the scaffold here, and use the login instead of the ID
94
+ # to access a user record. Using an ID gives outsiders a way to enumerate
95
+ # the users, which has security implications.
96
+ def list
97
+ options = {
98
+ :per_page => 10,
99
+ :order_by => 'name, id'
100
+ }
101
+
102
+ @user_pages, @users = paginate(:user, options)
103
+ end
104
+
105
+ # Attempt HTTP authentication, and fall back on a login form.
106
+ # If this method is called login_admin (it's an alias), keep trying
107
+ # until an administrator logs in or the user pushes the "back" button.
108
+ def login
109
+ if User.current and (admin? or action_name != 'login_admin')
110
+ redirect_back_or_default :action => :success
111
+ return
112
+ end
113
+
114
+ @user = User.new
115
+
116
+ http_authorize
117
+ end
118
+
119
+ # Log in an administrator. If a non-administrator logs in, keep trying
120
+ # until an administrator logs in or the user pushes the "back" button.
121
+ alias login_admin login
122
+
123
+ # Log out the current user, attempt HTTP authentication to log in a new
124
+ # user. The real reason we put up HTTP authentication here is to make
125
+ # the browser forget the old authentication data. Otherwise, the browser
126
+ # keeps sending it!
127
+ def logout
128
+ if User.current and @session[:new_login] != true
129
+ @session[:new_login] = true
130
+ User.sign_off
131
+ session[:user_id] = nil
132
+ @response.headers["Status"] = "Unauthorized"
133
+ @response.headers["WWW-Authenticate"] = \
134
+ "Basic realm=\"#{@request.domain(2)}\""
135
+ @user = User.new
136
+ render :action => 'login', :status => 401
137
+ else
138
+ @session[:new_login] = false
139
+ if User.current
140
+ redirect_to :action => 'success'
141
+ else
142
+ redirect_to :action => 'login'
143
+ end
144
+ end
145
+ end
146
+
147
+ # Create a new user.
148
+ def new
149
+ case @request.method
150
+ when :get
151
+ @user = User.new
152
+ when :post
153
+ @user = User.new(@params['user'])
154
+ # FIX: Save may fail. Create the email before the record is saved,
155
+ # deliver it afterward.
156
+ url = url_for(:controller => 'user', :action => 'activate', :id => @user.id, :token => @user.new_token)
157
+ UserMailer.deliver_new_user(@user, url)
158
+ if @user.save
159
+ flash['notice'] = 'User created.'
160
+ redirect_to :action => 'success'
161
+ else
162
+ flash['notice'] = 'Creation of new user failed.'
163
+ end
164
+ end
165
+ end
166
+
167
+ # Display a user's information.
168
+ # FIX: Use the user's login instead of the record ID.
169
+ # Using the record ID provides an easy way for outsiders to enumerate the
170
+ # users, which has security implications.
171
+ def show
172
+ @user = User.find(@params[:id])
173
+ end
174
+
175
+ # Tell the user that an action succeeded.
176
+ def success
177
+ end
178
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'user_controller'
3
+
4
+ # Raise errors beyond the default web-based presentation
5
+ class UserController; def rescue_action(e) raise e end; end
6
+
7
+ class UserControllerTest < Test::Unit::TestCase
8
+ fixtures :users
9
+
10
+ def setup
11
+ @controller = UserController.new
12
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
13
+ @request.host = "localhost"
14
+ end
15
+
16
+ def test_true
17
+ true
18
+ end
19
+
20
+ end