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