sinatra-security 0.1.6 → 0.2.0
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/.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
|