##// END OF EJS Templates
Allow "stay logged in" from multiple browsers (#10840)....
Jean-Philippe Lang -
r15792:5d4b5fd1f68a
parent child
Show More
@@ -1,93 +1,145
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Token < ActiveRecord::Base
18 class Token < ActiveRecord::Base
19 belongs_to :user
19 belongs_to :user
20 validates_uniqueness_of :value
20 validates_uniqueness_of :value
21 attr_protected :id
21 attr_protected :id
22
22
23 before_create :delete_previous_tokens, :generate_new_token
23 before_create :delete_previous_tokens, :generate_new_token
24
24
25 cattr_accessor :validity_time
25 cattr_accessor :validity_time
26 self.validity_time = 1.day
26 self.validity_time = 1.day
27
27
28 class << self
29 attr_reader :actions
30
31 def add_action(name, options)
32 options.assert_valid_keys(:max_instances, :validity_time)
33 @actions ||= {}
34 @actions[name.to_s] = options
35 end
36 end
37
38 add_action :api, max_instances: 1, validity_time: nil
39 add_action :autologin, max_instances: 10, validity_time: Proc.new { Setting.autologin.to_i.days }
40 add_action :feeds, max_instances: 1, validity_time: nil
41 add_action :recovery, max_instances: 1, validity_time: Proc.new { Token.validity_time }
42 add_action :register, max_instances: 1, validity_time: Proc.new { Token.validity_time }
43 add_action :session, max_instances: 10, validity_time: nil
44
28 def generate_new_token
45 def generate_new_token
29 self.value = Token.generate_token_value
46 self.value = Token.generate_token_value
30 end
47 end
31
48
32 # Return true if token has expired
49 # Return true if token has expired
33 def expired?
50 def expired?
34 return Time.now > self.created_on + self.class.validity_time
51 return created_on < self.class.invalid_when_created_before(action)
52 end
53
54 def max_instances
55 Token.actions.has_key?(action) ? Token.actions[action][:max_instances] : 1
56 end
57
58 def self.invalid_when_created_before(action = nil)
59 if Token.actions.has_key?(action)
60 validity_time = Token.actions[action][:validity_time]
61 validity_time = validity_time.call(action) if validity_time.respond_to? :call
62 else
63 validity_time = self.validity_time
64 end
65
66 if validity_time.nil?
67 0
68 else
69 Time.now - validity_time
70 end
35 end
71 end
36
72
37 # Delete all expired tokens
73 # Delete all expired tokens
38 def self.destroy_expired
74 def self.destroy_expired
39 Token.where("action NOT IN (?) AND created_on < ?", ['feeds', 'api', 'session'], Time.now - validity_time).delete_all
75 t = Token.arel_table
76
77 # Unknown actions have default validity_time
78 condition = t[:action].not_in(self.actions.keys).and(t[:created_on].lt(invalid_when_created_before))
79
80 self.actions.each do |action, options|
81 validity_time = invalid_when_created_before(action)
82
83 # Do not delete tokens, which don't become invalid
84 next if validity_time.nil?
85
86 condition = condition.or(
87 t[:action].eq(action).and(t[:created_on].lt(validity_time))
88 )
89 end
90
91 Token.where(condition).delete_all
40 end
92 end
41
93
42 # Returns the active user who owns the key for the given action
94 # Returns the active user who owns the key for the given action
43 def self.find_active_user(action, key, validity_days=nil)
95 def self.find_active_user(action, key, validity_days=nil)
44 user = find_user(action, key, validity_days)
96 user = find_user(action, key, validity_days)
45 if user && user.active?
97 if user && user.active?
46 user
98 user
47 end
99 end
48 end
100 end
49
101
50 # Returns the user who owns the key for the given action
102 # Returns the user who owns the key for the given action
51 def self.find_user(action, key, validity_days=nil)
103 def self.find_user(action, key, validity_days=nil)
52 token = find_token(action, key, validity_days)
104 token = find_token(action, key, validity_days)
53 if token
105 if token
54 token.user
106 token.user
55 end
107 end
56 end
108 end
57
109
58 # Returns the token for action and key with an optional
110 # Returns the token for action and key with an optional
59 # validity duration (in number of days)
111 # validity duration (in number of days)
60 def self.find_token(action, key, validity_days=nil)
112 def self.find_token(action, key, validity_days=nil)
61 action = action.to_s
113 action = action.to_s
62 key = key.to_s
114 key = key.to_s
63 return nil unless action.present? && key =~ /\A[a-z0-9]+\z/i
115 return nil unless action.present? && key =~ /\A[a-z0-9]+\z/i
64
116
65 token = Token.where(:action => action, :value => key).first
117 token = Token.where(:action => action, :value => key).first
66 if token && (token.action == action) && (token.value == key) && token.user
118 if token && (token.action == action) && (token.value == key) && token.user
67 if validity_days.nil? || (token.created_on > validity_days.days.ago)
119 if validity_days.nil? || (token.created_on > validity_days.days.ago)
68 token
120 token
69 end
121 end
70 end
122 end
71 end
123 end
72
124
73 def self.generate_token_value
125 def self.generate_token_value
74 Redmine::Utils.random_hex(20)
126 Redmine::Utils.random_hex(20)
75 end
127 end
76
128
77 private
129 private
78
130
79 # Removes obsolete tokens (same user and action)
131 # Removes obsolete tokens (same user and action)
80 def delete_previous_tokens
132 def delete_previous_tokens
81 if user
133 if user
82 scope = Token.where(:user_id => user.id, :action => action)
134 scope = Token.where(:user_id => user.id, :action => action)
83 if action == 'session'
135 if max_instances > 1
84 ids = scope.order(:updated_on => :desc).offset(9).ids
136 ids = scope.order(:updated_on => :desc).offset(max_instances - 1).ids
85 if ids.any?
137 if ids.any?
86 Token.delete(ids)
138 Token.delete(ids)
87 end
139 end
88 else
140 else
89 scope.delete_all
141 scope.delete_all
90 end
142 end
91 end
143 end
92 end
144 end
93 end
145 end
@@ -1,121 +1,136
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class TokenTest < ActiveSupport::TestCase
20 class TokenTest < ActiveSupport::TestCase
21 fixtures :tokens, :users, :email_addresses
21 fixtures :tokens, :users, :email_addresses
22
22
23 def test_create
23 def test_create
24 token = Token.new
24 token = Token.new
25 token.save
25 token.save
26 assert_equal 40, token.value.length
26 assert_equal 40, token.value.length
27 assert !token.expired?
27 assert !token.expired?
28 end
28 end
29
29
30 def test_create_should_remove_existing_tokens
30 def test_create_should_remove_existing_tokens
31 user = User.find(1)
31 user = User.find(1)
32 t1 = Token.create(:user => user, :action => 'autologin')
32 t1 = Token.create(:user => user, :action => 'register')
33 t2 = Token.create(:user => user, :action => 'autologin')
33 t2 = Token.create(:user => user, :action => 'register')
34 assert_not_equal t1.value, t2.value
34 assert_not_equal t1.value, t2.value
35 assert !Token.exists?(t1.id)
35 assert !Token.exists?(t1.id)
36 assert Token.exists?(t2.id)
36 assert Token.exists?(t2.id)
37 end
37 end
38
38
39 def test_create_session_token_should_keep_last_10_tokens
39 def test_create_session_or_autologin_token_should_keep_last_10_tokens
40 Token.delete_all
40 Token.delete_all
41 user = User.find(1)
41 user = User.find(1)
42
42
43 ["autologin", "session"].each do |action|
43 assert_difference 'Token.count', 10 do
44 assert_difference 'Token.count', 10 do
44 10.times { Token.create!(:user => user, :action => 'session') }
45 10.times { Token.create!(:user => user, :action => action) }
45 end
46 end
46
47
47 assert_no_difference 'Token.count' do
48 assert_no_difference 'Token.count' do
48 Token.create!(:user => user, :action => 'session')
49 Token.create!(:user => user, :action => action)
50 end
49 end
51 end
50 end
52 end
51
53
52 def test_destroy_expired_should_not_destroy_feeds_and_api_tokens
54 def test_destroy_expired_should_not_destroy_session_feeds_and_api_tokens
53 Token.delete_all
55 Token.delete_all
54
56
55 Token.create!(:user_id => 1, :action => 'api', :created_on => 7.days.ago)
57 Token.create!(:user_id => 1, :action => 'api', :created_on => 7.days.ago)
56 Token.create!(:user_id => 1, :action => 'feeds', :created_on => 7.days.ago)
58 Token.create!(:user_id => 1, :action => 'feeds', :created_on => 7.days.ago)
59 Token.create!(:user_id => 1, :action => 'session', :created_on => 7.days.ago)
57
60
58 assert_no_difference 'Token.count' do
61 assert_no_difference 'Token.count' do
59 assert_equal 0, Token.destroy_expired
62 assert_equal 0, Token.destroy_expired
60 end
63 end
61 end
64 end
62
65
63 def test_destroy_expired_should_destroy_expired_tokens
66 def test_destroy_expired_should_destroy_expired_tokens
64 Token.delete_all
67 Token.delete_all
65
68
66 Token.create!(:user_id => 1, :action => 'autologin', :created_on => 7.days.ago)
69 # Expiration of autologin tokens is determined by Setting.autologin
67 Token.create!(:user_id => 2, :action => 'autologin', :created_on => 3.days.ago)
70 Setting.autologin = "7"
68 Token.create!(:user_id => 3, :action => 'autologin', :created_on => 1.hour.ago)
71 Token.create!(:user_id => 2, :action => 'autologin', :created_on => 3.weeks.ago)
72 Token.create!(:user_id => 3, :action => 'autologin', :created_on => 3.days.ago)
73
74 # Expiration of register and recovery tokens is determined by Token.validity_time
75 Token.create!(:user_id => 1, :action => 'register', :created_on => 7.days.ago)
76 Token.create!(:user_id => 3, :action => 'register', :created_on => 7.hours.ago)
77
78 Token.create!(:user_id => 2, :action => 'recovery', :created_on => 3.days.ago)
79 Token.create!(:user_id => 3, :action => 'recovery', :created_on => 3.hours.ago)
80
81 # Expiration of tokens with unknown action is determined by Token.validity_time
82 Token.create!(:user_id => 2, :action => 'unknown_action', :created_on => 2.days.ago)
83 Token.create!(:user_id => 3, :action => 'unknown_action', :created_on => 2.hours.ago)
69
84
70 assert_difference 'Token.count', -2 do
85 assert_difference 'Token.count', -4 do
71 assert_equal 2, Token.destroy_expired
86 assert_equal 4, Token.destroy_expired
72 end
87 end
73 end
88 end
74
89
75 def test_find_active_user_should_return_user
90 def test_find_active_user_should_return_user
76 token = Token.create!(:user_id => 1, :action => 'api')
91 token = Token.create!(:user_id => 1, :action => 'api')
77 assert_equal User.find(1), Token.find_active_user('api', token.value)
92 assert_equal User.find(1), Token.find_active_user('api', token.value)
78 end
93 end
79
94
80 def test_find_active_user_should_return_nil_for_locked_user
95 def test_find_active_user_should_return_nil_for_locked_user
81 token = Token.create!(:user_id => 1, :action => 'api')
96 token = Token.create!(:user_id => 1, :action => 'api')
82 User.find(1).lock!
97 User.find(1).lock!
83 assert_nil Token.find_active_user('api', token.value)
98 assert_nil Token.find_active_user('api', token.value)
84 end
99 end
85
100
86 def test_find_user_should_return_user
101 def test_find_user_should_return_user
87 token = Token.create!(:user_id => 1, :action => 'api')
102 token = Token.create!(:user_id => 1, :action => 'api')
88 assert_equal User.find(1), Token.find_user('api', token.value)
103 assert_equal User.find(1), Token.find_user('api', token.value)
89 end
104 end
90
105
91 def test_find_user_should_return_locked_user
106 def test_find_user_should_return_locked_user
92 token = Token.create!(:user_id => 1, :action => 'api')
107 token = Token.create!(:user_id => 1, :action => 'api')
93 User.find(1).lock!
108 User.find(1).lock!
94 assert_equal User.find(1), Token.find_user('api', token.value)
109 assert_equal User.find(1), Token.find_user('api', token.value)
95 end
110 end
96
111
97 def test_find_token_should_return_the_token
112 def test_find_token_should_return_the_token
98 token = Token.create!(:user_id => 1, :action => 'api')
113 token = Token.create!(:user_id => 1, :action => 'api')
99 assert_equal token, Token.find_token('api', token.value)
114 assert_equal token, Token.find_token('api', token.value)
100 end
115 end
101
116
102 def test_find_token_should_return_the_token_with_validity
117 def test_find_token_should_return_the_token_with_validity
103 token = Token.create!(:user_id => 1, :action => 'api', :created_on => 1.hour.ago)
118 token = Token.create!(:user_id => 1, :action => 'api', :created_on => 1.hour.ago)
104 assert_equal token, Token.find_token('api', token.value, 1)
119 assert_equal token, Token.find_token('api', token.value, 1)
105 end
120 end
106
121
107 def test_find_token_should_return_nil_with_wrong_action
122 def test_find_token_should_return_nil_with_wrong_action
108 token = Token.create!(:user_id => 1, :action => 'feeds')
123 token = Token.create!(:user_id => 1, :action => 'feeds')
109 assert_nil Token.find_token('api', token.value)
124 assert_nil Token.find_token('api', token.value)
110 end
125 end
111
126
112 def test_find_token_should_return_nil_without_user
127 def test_find_token_should_return_nil_without_user
113 token = Token.create!(:user_id => 999, :action => 'api')
128 token = Token.create!(:user_id => 999, :action => 'api')
114 assert_nil Token.find_token('api', token.value)
129 assert_nil Token.find_token('api', token.value)
115 end
130 end
116
131
117 def test_find_token_should_return_nil_with_validity_expired
132 def test_find_token_should_return_nil_with_validity_expired
118 token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
133 token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
119 assert_nil Token.find_token('api', token.value, 1)
134 assert_nil Token.find_token('api', token.value, 1)
120 end
135 end
121 end
136 end
General Comments 0
You need to be logged in to leave comments. Login now