sinatra-security 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/VERSION +1 -1
- data/lib/sinatra/security.rb +36 -3
- data/lib/sinatra/security/helpers.rb +111 -30
- data/lib/sinatra/security/identification.rb +44 -6
- data/lib/sinatra/security/login_field.rb +36 -3
- data/lib/sinatra/security/password.rb +76 -24
- data/lib/sinatra/security/user.rb +26 -1
- data/lib/sinatra/security/validations.rb +8 -5
- data/sinatra-security.gemspec +5 -3
- data/test/test_login_field_flexibility.rb +37 -0
- data/test/test_password.rb +15 -3
- data/test/test_sinatra-security.rb +29 -0
- data/test/test_sinatra_security_helpers.rb +27 -3
- data/test/test_validations.rb +58 -0
- metadata +14 -5
data/.gitignore
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/sinatra/security.rb
CHANGED
@@ -2,6 +2,8 @@ require 'sinatra/base'
|
|
2
2
|
|
3
3
|
module Sinatra
|
4
4
|
module Security
|
5
|
+
VERSION = "0.2.0"
|
6
|
+
|
5
7
|
autoload :Helpers, 'sinatra/security/helpers'
|
6
8
|
autoload :User, 'sinatra/security/user'
|
7
9
|
autoload :Validations, 'sinatra/security/validations'
|
@@ -12,12 +14,43 @@ module Sinatra
|
|
12
14
|
def self.registered(app)
|
13
15
|
app.helpers Helpers
|
14
16
|
|
17
|
+
app.set :login_error_message, "Wrong Email and/or Password combination."
|
18
|
+
app.set :login_url, "/login"
|
19
|
+
app.set :login_user_class, :User
|
20
|
+
app.set :ignored_by_return_to, /(jpe?g|png|gif|css|js)$/
|
21
|
+
|
15
22
|
app.post '/login' do
|
16
23
|
if authenticate(params)
|
17
|
-
|
24
|
+
redirect_to_return_url
|
18
25
|
else
|
19
|
-
session[:error] =
|
20
|
-
redirect
|
26
|
+
session[:error] = settings.login_error_message
|
27
|
+
redirect settings.login_url
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Allows you to declaratively declare secured locations based on their
|
33
|
+
# path prefix.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
#
|
37
|
+
# require_login '/admin'
|
38
|
+
#
|
39
|
+
# get '/admin/posts' do
|
40
|
+
# # Posts here
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# get '/admin/users' do
|
44
|
+
# # Users here
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @param [#to_s] path_prefix a string to match again the start of
|
48
|
+
# request.fullpath
|
49
|
+
# @return [nil]
|
50
|
+
def require_login(path_prefix)
|
51
|
+
before do
|
52
|
+
if request.fullpath =~ /^#{path_prefix}/
|
53
|
+
require_login
|
21
54
|
end
|
22
55
|
end
|
23
56
|
end
|
@@ -1,51 +1,132 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Security
|
3
3
|
module Helpers
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
# The main gateway. This method will redirect if no user is currently
|
5
|
+
# authenticated.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# get '/secured' do
|
10
|
+
# require_login
|
11
|
+
#
|
12
|
+
# # do super private thing here
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @param [String] login_url (defaults to /login) the url of the login form.
|
16
|
+
# take not that even if you specify a different login form,
|
17
|
+
# the POST action for that form should still be '/login'.
|
18
|
+
def require_login(login_url = settings.login_url)
|
19
|
+
return if logged_in?
|
12
20
|
|
13
|
-
|
14
|
-
|
15
|
-
session[:user] = user.id
|
21
|
+
if should_return_to?(request.fullpath)
|
22
|
+
session[:return_to] = request.fullpath
|
16
23
|
end
|
24
|
+
redirect login_url
|
17
25
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
|
27
|
+
# Dynamic redirection based on the return path that was set.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
#
|
31
|
+
# # By default assumes you use :return_to and '/'.
|
32
|
+
# # You can use this in your code as well. i.e.
|
33
|
+
# get '/fb/login' do
|
34
|
+
# session[:fb_return_to] = params[:from]
|
35
|
+
# # redirect to fb OAuth URI here.
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# get '/fb/success' do
|
39
|
+
# # successfully processed, save whatever here
|
40
|
+
# redirect_to_return_url :fb_return_to, "/home"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @param [Symbol] session_key the key in the session, defaults to
|
44
|
+
# :return_to.
|
45
|
+
# @param [String] default url when no stored value is found in
|
46
|
+
# session[session_key]. defaults to '/'.
|
47
|
+
def redirect_to_return_url(session_key = :return_to, default = '/')
|
48
|
+
redirect session.delete(:return_to) || default
|
29
49
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
50
|
+
|
51
|
+
# Returns the currently logged in user, identified through
|
52
|
+
# session[:user]. The default finder uses User[id], based on Ohm's
|
53
|
+
# finder method.
|
54
|
+
#
|
55
|
+
# @see http://ohm.keyvalue.org
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
#
|
59
|
+
# # ActiveRecord style finders
|
60
|
+
# current_user(lambda { |id| User.find(id) })
|
61
|
+
#
|
62
|
+
# # Also, if you change the settings to use a different user class,
|
63
|
+
# # then that will be respected
|
64
|
+
# set :login_user_class, :SuperUser
|
65
|
+
#
|
66
|
+
# # assuming session[:user] == 1
|
67
|
+
# current_user == SuperUser[1]
|
68
|
+
# # => true
|
69
|
+
#
|
70
|
+
# @param [Proc] finder (defaults to User[id]) allows you to pass in a
|
71
|
+
# different finder method.
|
72
|
+
# @return [User] or alternatively, an instance of settings.login_user_class
|
73
|
+
def current_user(finder = lambda { |id| __USER__[id] })
|
74
|
+
@current_user ||= finder.call(session[:user]) if session[:user]
|
33
75
|
end
|
34
|
-
|
76
|
+
|
77
|
+
# @return [true] if the user is logged in
|
78
|
+
# @return [false] if the user is not logged in
|
35
79
|
def logged_in?
|
36
80
|
!! current_user
|
37
81
|
end
|
38
|
-
|
82
|
+
|
83
|
+
# Used for simple atomic authorization rules on a per action / route
|
84
|
+
# basis.
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
#
|
88
|
+
# get '/posts/:id/edit' do |id|
|
89
|
+
# post = Post[id]
|
90
|
+
# ensure_current_user post.author # halts to a 404 if not satisfied.
|
91
|
+
#
|
92
|
+
# # the rest of this gets executed when
|
93
|
+
# # the author is indeed the current user.
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# @param [User] a user object.
|
39
97
|
def ensure_current_user(user)
|
40
98
|
halt 404 unless user == current_user
|
41
99
|
end
|
42
|
-
|
100
|
+
|
101
|
+
# The method says it all. Mostly for keeping responsibility where it
|
102
|
+
# belongs, instead of letting the application code deal with the session
|
103
|
+
# keys themselves.
|
43
104
|
def logout!
|
44
105
|
session.delete(:user)
|
45
106
|
end
|
107
|
+
|
108
|
+
# Internally used by the POST /login route handler.
|
109
|
+
#
|
110
|
+
# @param [Hash] opts The hash containing :username and :password.
|
111
|
+
# @option opts [#to_s] :username The username of a User.
|
112
|
+
# @option opts [String] :password The password of a User.
|
113
|
+
# @return [String] the `id` of the user if found.
|
114
|
+
# @return [nil] if no user matches the :username / :password combination.
|
115
|
+
def authenticate(opts)
|
116
|
+
if user = __USER__.authenticate(opts[:username], opts[:password])
|
117
|
+
session[:user] = user.id
|
118
|
+
end
|
119
|
+
end
|
46
120
|
|
47
|
-
|
48
|
-
|
121
|
+
# @private transforms settings.login_user_class to a constant,
|
122
|
+
# and used by current_user
|
123
|
+
def __USER__
|
124
|
+
Object.const_get(settings.login_user_class)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @private internally used by Sinatra::Security::Helpers#require_login
|
128
|
+
def should_return_to?(path, ignored = settings.ignored_by_return_to)
|
129
|
+
!(path =~ ignored)
|
49
130
|
end
|
50
131
|
end
|
51
132
|
end
|
@@ -1,17 +1,55 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Security
|
3
|
+
# The identification module is mixed into Sinatra::Security::User. The
|
4
|
+
# API loosely applies to Ohm, although it will work with ActiveRecord.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# class User < Ohm::Model
|
9
|
+
# include Sinatra::Security::User
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# User.respond_to?(:find_by_login)
|
13
|
+
# # => true
|
14
|
+
#
|
15
|
+
# User.respond_to?(:authenticate)
|
16
|
+
# # => true
|
17
|
+
#
|
18
|
+
# If you wish to override any of these, it's as simple as defining your
|
19
|
+
# own method in your User class.
|
3
20
|
module Identification
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
21
|
+
# Finds the User matching the given login / password combination.
|
22
|
+
# @see Sinatra::Security::Helpers#authenticate
|
23
|
+
#
|
24
|
+
# @param [#to_s] login the value of `:email` in your datastore.
|
25
|
+
# @param [String] password the raw password for the user in `:email`.
|
26
|
+
# @return [User] the user matching the credentials if found.
|
27
|
+
# @return [nil] if no matching user found.
|
28
|
+
def authenticate(login, password)
|
9
29
|
if user = find_by_login(login)
|
10
|
-
if Sinatra::Security::Password::Hashing.check(
|
30
|
+
if Sinatra::Security::Password::Hashing.check(password, user.crypted_password)
|
11
31
|
user
|
12
32
|
end
|
13
33
|
end
|
14
34
|
end
|
35
|
+
|
36
|
+
# Used internally by User::authenticate to find the user given the
|
37
|
+
# `:email` value.
|
38
|
+
#
|
39
|
+
# @param [#to_s] login the value of `:email` in your datastore. You may
|
40
|
+
# also override the key used.
|
41
|
+
# @return [User] an instance of User if found.
|
42
|
+
# @return [nil] if no user found with the given login value.
|
43
|
+
#
|
44
|
+
# @see Sinatra::Security::LoginField::attr_name
|
45
|
+
def find_by_login(login)
|
46
|
+
find(__LOGIN_FIELD__ => login).first
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
def __LOGIN_FIELD__
|
51
|
+
Sinatra::Security::LoginField.attr_name
|
52
|
+
end
|
15
53
|
end
|
16
54
|
end
|
17
55
|
end
|
@@ -1,11 +1,44 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Security
|
3
|
+
# This module allows you to customize the name of the login field used
|
4
|
+
# in your datastore. By default :email is used.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# # somewhere in your code during bootstrapping
|
9
|
+
# require 'sinatra/security'
|
10
|
+
# Sinatra::Security::LoginField.attr_name :login
|
11
|
+
#
|
12
|
+
# # then in your actual user.rb or something...
|
13
|
+
# class User < Ohm::Model
|
14
|
+
# include Sinatra::Security::User
|
15
|
+
# # at this point the following are done:
|
16
|
+
# # attribute :login
|
17
|
+
# # index :login
|
18
|
+
# end
|
3
19
|
module LoginField
|
20
|
+
# @example
|
21
|
+
#
|
22
|
+
# Sinatra::Security::LoginField.attr_name :username
|
23
|
+
#
|
24
|
+
# class User < Ohm::Model
|
25
|
+
# include Sinatra::Security::User
|
26
|
+
# # effectively executes the following:
|
27
|
+
# # attribute :username
|
28
|
+
# # index :username
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# @overload attr_name()
|
32
|
+
# Get the value of attr_name.
|
33
|
+
# @overload attr_name(attr_name)
|
34
|
+
# @param [#to_sym] attr_name the attr_name to be used e.g.
|
35
|
+
# :username, :login.
|
36
|
+
# @return [Symbol] the current attr_name.
|
4
37
|
def self.attr_name(attr_name = nil)
|
5
|
-
@attr_name = attr_name if attr_name
|
38
|
+
@attr_name = attr_name.to_sym if attr_name
|
6
39
|
@attr_name
|
7
40
|
end
|
8
|
-
attr_name
|
41
|
+
attr_name :email
|
9
42
|
|
10
43
|
def self.included(user)
|
11
44
|
user.attribute LoginField.attr_name
|
@@ -13,4 +46,4 @@ module Sinatra
|
|
13
46
|
end
|
14
47
|
end
|
15
48
|
end
|
16
|
-
end
|
49
|
+
end
|
@@ -2,16 +2,36 @@ require 'digest/sha2'
|
|
2
2
|
|
3
3
|
module Sinatra
|
4
4
|
module Security
|
5
|
+
# This module handles everything related to password handling.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# class User < Ohm::Model
|
10
|
+
# include Sinatra::Security::Password
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# User.attributes == [:crypted_password]
|
14
|
+
# # => true
|
15
|
+
#
|
16
|
+
# User.new.respond_to?(:password)
|
17
|
+
# # => true
|
18
|
+
#
|
19
|
+
# User.new.respond_to?(:password_confirmation)
|
20
|
+
# # => true
|
21
|
+
#
|
22
|
+
# @see http://ohm.keyvalue.org for information about Ohm::Model.
|
5
23
|
module Password
|
6
|
-
def self.included(
|
7
|
-
|
8
|
-
|
24
|
+
def self.included(model)
|
25
|
+
model.attribute :crypted_password
|
26
|
+
|
27
|
+
model.send :attr_accessor, :password, :password_confirmation
|
9
28
|
end
|
10
29
|
|
11
30
|
protected
|
31
|
+
# @private internally called by Ohm after validation when persisting.
|
12
32
|
def write
|
13
33
|
if !password.to_s.empty?
|
14
|
-
write_local :crypted_password,
|
34
|
+
write_local :crypted_password, Hashing.encrypt(password)
|
15
35
|
end
|
16
36
|
|
17
37
|
super
|
@@ -20,24 +40,61 @@ module Sinatra
|
|
20
40
|
module Hashing
|
21
41
|
extend self
|
22
42
|
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
43
|
+
# Given any string generates a string which includes both the
|
44
|
+
# 128 character (SHA512) hash and the salt.
|
45
|
+
#
|
46
|
+
# By default the salt is a 64 character pseudo-random string.
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
#
|
50
|
+
# include Sinatra::Security
|
51
|
+
#
|
52
|
+
# crypted = Password::Hashing.encrypt('123')
|
53
|
+
# crypted.length == 192
|
54
|
+
# # => true
|
55
|
+
#
|
56
|
+
# crypted = Password::Hashing.encrypt('123', '123456789012')
|
57
|
+
# crypted.length == 140 # i.e. 128 + 12 chars
|
58
|
+
# # => true
|
59
|
+
#
|
60
|
+
# crypted = Password::Hashing.encrypt('123')
|
61
|
+
# Password::Hashing.check('123', crypted)
|
62
|
+
# # => true
|
63
|
+
#
|
64
|
+
#
|
65
|
+
# @param [String] password any string with any length.
|
66
|
+
# @param [String] salt (defaults to Hashing#salt) any string.
|
67
|
+
# @return [String] a string holding the crypted password and the salt.
|
68
|
+
#
|
69
|
+
# @see Sinatra::Security::Password::Hashing#check
|
70
|
+
def encrypt(password, salt = self.generate_salt)
|
71
|
+
serialize(hash(password, salt), salt)
|
28
72
|
end
|
29
73
|
|
30
|
-
# Checks the password against the
|
31
|
-
|
32
|
-
|
33
|
-
|
74
|
+
# Checks the password against the serialized password.
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
#
|
78
|
+
# include Sinatra::Security
|
79
|
+
# crypted = Password::Hashing.encrypt('123')
|
80
|
+
# Password::Hashing.check('123', crypted)
|
81
|
+
# # => true
|
82
|
+
#
|
83
|
+
# @param [String] password a string to check against crypted.
|
84
|
+
# @param [String] crypted the serialized string containing the
|
85
|
+
# hash and salt.
|
86
|
+
#
|
87
|
+
# @return [true] if the password matches the hash / salt combination.
|
88
|
+
# @return [false] if the password does not match the hash / salt.
|
89
|
+
def check(password, crypted)
|
90
|
+
hash, salt = unserialize(crypted)
|
34
91
|
|
35
92
|
self.hash(password, salt) == hash
|
36
93
|
end
|
37
94
|
|
38
95
|
protected
|
39
96
|
# Generates a psuedo-random 64 character string
|
40
|
-
def
|
97
|
+
def generate_salt
|
41
98
|
salt = ""
|
42
99
|
64.times {
|
43
100
|
salt << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }
|
@@ -50,18 +107,13 @@ module Sinatra
|
|
50
107
|
end
|
51
108
|
|
52
109
|
# Mixes the hash and salt together for storage
|
53
|
-
def
|
110
|
+
def serialize(hash, salt)
|
54
111
|
hash + salt
|
55
112
|
end
|
56
|
-
|
57
|
-
#
|
58
|
-
def
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
# Gets the salt from a stored password
|
63
|
-
def get_salt(stored)
|
64
|
-
stored[128..192]
|
113
|
+
|
114
|
+
# Returns the original hash and salt generated from serialize.
|
115
|
+
def unserialize(serialized)
|
116
|
+
return serialized[0..127], serialized[128..-1]
|
65
117
|
end
|
66
118
|
end
|
67
119
|
end
|
@@ -1,5 +1,30 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Security
|
3
|
+
# Mixes in to any included class all of the following:
|
4
|
+
#
|
5
|
+
# * Sinatra::Security::LoginField
|
6
|
+
# * Sinatra::Security::Password
|
7
|
+
# * Sinatra::Security::Validations
|
8
|
+
#
|
9
|
+
# It also extends the class with Sinatra::Security::Identification.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# class User < Ohm::Model
|
14
|
+
# include Sinatra::Security::User
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# user = User.new
|
18
|
+
# user.valid?
|
19
|
+
# user.errors == [[:email, :not_present], [:password, :not_present]]
|
20
|
+
# # => true
|
21
|
+
#
|
22
|
+
# user = User.create(:email => "test@example.com", :password => "pass",
|
23
|
+
# :password_confirmation => "pass")
|
24
|
+
#
|
25
|
+
# User.authenticate("test@example.com", "pass") == user
|
26
|
+
# # => true
|
27
|
+
#
|
3
28
|
module User
|
4
29
|
def self.included(user)
|
5
30
|
user.send :include, LoginField
|
@@ -10,4 +35,4 @@ module Sinatra
|
|
10
35
|
end
|
11
36
|
end
|
12
37
|
end
|
13
|
-
end
|
38
|
+
end
|
@@ -4,11 +4,14 @@ module Sinatra
|
|
4
4
|
EMAIL_FORMAT = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
|
5
5
|
|
6
6
|
def validate
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
login_field = Sinatra::Security::LoginField.attr_name
|
8
|
+
|
9
|
+
if login_field == :email
|
10
|
+
assert_login_using_email :email
|
11
|
+
else
|
12
|
+
assert_present(login_field) and assert_unique(login_field)
|
13
|
+
end
|
14
|
+
|
12
15
|
assert_password :password
|
13
16
|
|
14
17
|
super
|
data/sinatra-security.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{sinatra-security}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Cyril David"]
|
12
|
-
s.date = %q{2010-05-
|
12
|
+
s.date = %q{2010-05-26}
|
13
13
|
s.description = %q{For use with Sinatra + Monk + OHM}
|
14
14
|
s.email = %q{cyx.ucron@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
|
|
35
35
|
"lib/sinatra/security/validations.rb",
|
36
36
|
"sinatra-security.gemspec",
|
37
37
|
"test/helper.rb",
|
38
|
+
"test/test_login_field_flexibility.rb",
|
38
39
|
"test/test_password.rb",
|
39
40
|
"test/test_sinatra-security.rb",
|
40
41
|
"test/test_sinatra_security_helpers.rb",
|
@@ -44,10 +45,11 @@ Gem::Specification.new do |s|
|
|
44
45
|
s.homepage = %q{http://github.com/sinefunc/sinatra-security}
|
45
46
|
s.rdoc_options = ["--charset=UTF-8"]
|
46
47
|
s.require_paths = ["lib"]
|
47
|
-
s.rubygems_version = %q{1.3.
|
48
|
+
s.rubygems_version = %q{1.3.6}
|
48
49
|
s.summary = %q{Sinatra authentication extension}
|
49
50
|
s.test_files = [
|
50
51
|
"test/helper.rb",
|
52
|
+
"test/test_login_field_flexibility.rb",
|
51
53
|
"test/test_password.rb",
|
52
54
|
"test/test_sinatra-security.rb",
|
53
55
|
"test/test_sinatra_security_helpers.rb",
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class LoginFieldFlexbilityTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
Sinatra::Security::LoginField.attr_name :login
|
6
|
+
end
|
7
|
+
|
8
|
+
def teardown
|
9
|
+
Sinatra::Security::LoginField.attr_name :email
|
10
|
+
end
|
11
|
+
|
12
|
+
class DiffUser
|
13
|
+
def self.attribute(att)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.index(att)
|
17
|
+
end
|
18
|
+
|
19
|
+
include Sinatra::Security::User
|
20
|
+
end
|
21
|
+
|
22
|
+
test "find_by_login uses :login" do
|
23
|
+
@set = stub("Set", :first => :user)
|
24
|
+
DiffUser.expects(:find).with(:login => 'foobar').returns(@set)
|
25
|
+
|
26
|
+
assert_equal :user, DiffUser.find_by_login('foobar')
|
27
|
+
end
|
28
|
+
|
29
|
+
test "authenticate method uses :login" do
|
30
|
+
@user = stub("User", :id => 1001, :crypted_password => '_crypted_')
|
31
|
+
@set = stub("Set", :first => @user)
|
32
|
+
DiffUser.expects(:find).with(:login => 'foobar').returns(@set)
|
33
|
+
|
34
|
+
|
35
|
+
DiffUser.authenticate('foobar', 'pass')
|
36
|
+
end
|
37
|
+
end
|
data/test/test_password.rb
CHANGED
@@ -21,8 +21,13 @@ class TestPassword < Test::Unit::TestCase
|
|
21
21
|
include Sinatra::Security
|
22
22
|
|
23
23
|
describe "#update" do
|
24
|
-
should "return a 192 character string" do
|
25
|
-
assert_equal 192, Password::Hashing.
|
24
|
+
should "return a 192 character string by default" do
|
25
|
+
assert_equal 192, Password::Hashing.encrypt('123456').length
|
26
|
+
end
|
27
|
+
|
28
|
+
should "return a different length if you pass a custom salt" do
|
29
|
+
assert_equal 140,
|
30
|
+
Password::Hashing.encrypt('123456', '123456789012').length
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
@@ -30,7 +35,14 @@ class TestPassword < Test::Unit::TestCase
|
|
30
35
|
should "be able to match the original password given" do
|
31
36
|
@password = 'password100'
|
32
37
|
|
33
|
-
assert Password::Hashing.check(@password, Password::Hashing.
|
38
|
+
assert Password::Hashing.check(@password, Password::Hashing.encrypt(@password))
|
39
|
+
end
|
40
|
+
|
41
|
+
should "be able to match original pass given custom salt" do
|
42
|
+
password = 'password100'
|
43
|
+
crypted = Password::Hashing.encrypt(password, 'customsalt')
|
44
|
+
|
45
|
+
assert Password::Hashing.check(password, crypted)
|
34
46
|
end
|
35
47
|
end
|
36
48
|
|
@@ -5,6 +5,8 @@ class BasicApp < Sinatra::Base
|
|
5
5
|
|
6
6
|
register Sinatra::Security
|
7
7
|
|
8
|
+
require_login '/mass'
|
9
|
+
|
8
10
|
get '/login' do
|
9
11
|
"<h1>Login Page</h1>"
|
10
12
|
end
|
@@ -16,6 +18,14 @@ class BasicApp < Sinatra::Base
|
|
16
18
|
get '/private' do
|
17
19
|
require_login
|
18
20
|
end
|
21
|
+
|
22
|
+
get '/mass/private1' do
|
23
|
+
"Private 1"
|
24
|
+
end
|
25
|
+
|
26
|
+
get '/mass/private2' do
|
27
|
+
"Private 2"
|
28
|
+
end
|
19
29
|
|
20
30
|
get '/css/main.css' do
|
21
31
|
require_login
|
@@ -136,4 +146,23 @@ class TestSinatraSecurity < Test::Unit::TestCase
|
|
136
146
|
assert_equal '/login', last_response.headers['Location']
|
137
147
|
end
|
138
148
|
end
|
149
|
+
|
150
|
+
describe "going to /mass/private1" do
|
151
|
+
should "redirect to /login" do
|
152
|
+
get '/mass/private1'
|
153
|
+
|
154
|
+
assert_equal 302, last_response.status
|
155
|
+
assert_equal '/login', last_response.headers['Location']
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "going to /mass/private2" do
|
160
|
+
should "redirect to /login" do
|
161
|
+
get '/mass/private2'
|
162
|
+
|
163
|
+
assert_equal 302, last_response.status
|
164
|
+
assert_equal '/login', last_response.headers['Location']
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
139
168
|
end
|
@@ -27,6 +27,9 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
|
|
27
27
|
|
28
28
|
describe "when session[:user] is set to 1" do
|
29
29
|
setup do
|
30
|
+
@settings = stub("Settings", :login_user_class => :User)
|
31
|
+
|
32
|
+
@context.stubs(:settings).returns(@settings)
|
30
33
|
@context.session[:user] = 1
|
31
34
|
end
|
32
35
|
|
@@ -83,15 +86,27 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
|
|
83
86
|
|
84
87
|
describe "#require_login" do
|
85
88
|
context "when logged_in?" do
|
86
|
-
should "
|
89
|
+
should "not redirect" do
|
90
|
+
@settings = stub("Settings", :ignored_by_return_to => /foobared$/,
|
91
|
+
:login_url => '/new-login-url')
|
92
|
+
@context.stubs(:settings).returns(@settings)
|
93
|
+
|
94
|
+
|
95
|
+
@context.expects(:redirect).never
|
87
96
|
@context.expects(:logged_in?).returns(true)
|
88
|
-
|
97
|
+
|
98
|
+
@context.require_login
|
89
99
|
end
|
90
100
|
end
|
91
101
|
|
92
102
|
context "when not logged_in?" do
|
93
103
|
setup do
|
94
104
|
@context.stubs(:logged_in?).returns(false)
|
105
|
+
@settings = stub("Settings", :ignored_by_return_to => /foobared$/,
|
106
|
+
:login_url => '/new-login-url')
|
107
|
+
|
108
|
+
@context.stubs(:settings).returns(@settings)
|
109
|
+
|
95
110
|
@context.request = stub("Request", :fullpath => "/some/fullpath/here")
|
96
111
|
end
|
97
112
|
|
@@ -102,7 +117,7 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
|
|
102
117
|
end
|
103
118
|
|
104
119
|
should "redirect to /login" do
|
105
|
-
@context.expects(:redirect).with('/login')
|
120
|
+
@context.expects(:redirect).with('/new-login-url')
|
106
121
|
|
107
122
|
@context.require_login
|
108
123
|
end
|
@@ -110,6 +125,15 @@ class TestSinatraSecurityHelpers < Test::Unit::TestCase
|
|
110
125
|
should "return false" do
|
111
126
|
assert ! @context.require_login
|
112
127
|
end
|
128
|
+
|
129
|
+
context "when the request matches ignored" do
|
130
|
+
should "not store the fullpath in return_to" do
|
131
|
+
@context.request = stub("Request", :fullpath => "/some/fullpath/foobared")
|
132
|
+
@context.require_login
|
133
|
+
|
134
|
+
assert_nil @context.session[:return_to]
|
135
|
+
end
|
136
|
+
end
|
113
137
|
end
|
114
138
|
end
|
115
139
|
|
data/test/test_validations.rb
CHANGED
@@ -46,6 +46,64 @@ class UserDefaultValidation < TestFixtures::User
|
|
46
46
|
end
|
47
47
|
|
48
48
|
class TestValidations < Test::Unit::TestCase
|
49
|
+
context "with a different login field" do
|
50
|
+
class User < TestFixtures::User
|
51
|
+
include Sinatra::Security::Validations
|
52
|
+
|
53
|
+
attr_accessor :login
|
54
|
+
|
55
|
+
def errors
|
56
|
+
@errors ||= []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
setup do
|
61
|
+
Sinatra::Security::LoginField.attr_name :login
|
62
|
+
end
|
63
|
+
|
64
|
+
teardown do
|
65
|
+
Sinatra::Security::LoginField.attr_name :email
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when a new record" do
|
69
|
+
should "assert login present" do
|
70
|
+
user = User.new
|
71
|
+
user.expects(:new?).returns(true)
|
72
|
+
user.validate
|
73
|
+
|
74
|
+
assert_equal [[:login, :not_present], [:password, :not_present]],
|
75
|
+
user.errors
|
76
|
+
end
|
77
|
+
|
78
|
+
should "assert login unique" do
|
79
|
+
user = User.new
|
80
|
+
user.login = 'login'
|
81
|
+
user.expects(:new?).returns(true)
|
82
|
+
user.expects(:assert_unique).with(:login)
|
83
|
+
user.validate
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when an existing record" do
|
88
|
+
should "still assert login present" do
|
89
|
+
user = User.new
|
90
|
+
user.expects(:new?).returns(false)
|
91
|
+
user.validate
|
92
|
+
|
93
|
+
assert_equal [[:login, :not_present]], user.errors
|
94
|
+
end
|
95
|
+
|
96
|
+
should "assert login unique" do
|
97
|
+
user = User.new
|
98
|
+
user.login = 'login'
|
99
|
+
user.expects(:new?).returns(false)
|
100
|
+
user.expects(:assert_unique).with(:login)
|
101
|
+
user.validate
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
49
107
|
context "without an email" do
|
50
108
|
should "require the email to be present" do
|
51
109
|
user = UserWithEmailValidation.new
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinatra-security
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Cyril David
|
@@ -9,7 +14,7 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-05-
|
17
|
+
date: 2010-05-26 00:00:00 +08:00
|
13
18
|
default_executable:
|
14
19
|
dependencies: []
|
15
20
|
|
@@ -41,6 +46,7 @@ files:
|
|
41
46
|
- lib/sinatra/security/validations.rb
|
42
47
|
- sinatra-security.gemspec
|
43
48
|
- test/helper.rb
|
49
|
+
- test/test_login_field_flexibility.rb
|
44
50
|
- test/test_password.rb
|
45
51
|
- test/test_sinatra-security.rb
|
46
52
|
- test/test_sinatra_security_helpers.rb
|
@@ -59,23 +65,26 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
65
|
requirements:
|
60
66
|
- - ">="
|
61
67
|
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
62
70
|
version: "0"
|
63
|
-
version:
|
64
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
72
|
requirements:
|
66
73
|
- - ">="
|
67
74
|
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 0
|
68
77
|
version: "0"
|
69
|
-
version:
|
70
78
|
requirements: []
|
71
79
|
|
72
80
|
rubyforge_project:
|
73
|
-
rubygems_version: 1.3.
|
81
|
+
rubygems_version: 1.3.6
|
74
82
|
signing_key:
|
75
83
|
specification_version: 3
|
76
84
|
summary: Sinatra authentication extension
|
77
85
|
test_files:
|
78
86
|
- test/helper.rb
|
87
|
+
- test/test_login_field_flexibility.rb
|
79
88
|
- test/test_password.rb
|
80
89
|
- test/test_sinatra-security.rb
|
81
90
|
- test/test_sinatra_security_helpers.rb
|