model_security_generator 0.0.1

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