Project

General

Profile

Feature #43938 » 0001-track-api-key-last-used-on.patch

Vincent Robert, 2026-04-09 17:53

View differences:

app/models/token.rb
125 125
    token
126 126
  end
127 127

  
128
  def self.touch_api_token(key)
129
    where(action: 'api', value: key)
130
      .where("last_used_on IS NULL OR last_used_on <= ?", 1.minute.ago)
131
      .update_all(last_used_on: Time.now.utc)
132
  end
133

  
128 134
  def self.generate_token_value
129 135
    Redmine::Utils.random_hex(20)
130 136
  end
app/models/user.rb
568 568
  end
569 569

  
570 570
  def self.find_by_api_key(key)
571
    Token.find_active_user('api', key)
571
    user = Token.find_active_user('api', key)
572
    Token.touch_api_token(key) if user
573
    user
572 574
  end
573 575

  
574 576
  # Makes find_by_mail case-insensitive
app/views/my/_sidebar.html.erb
38 38
  <%= javascript_tag("$('#api-access-key').hide();") %>
39 39
  <p>
40 40
  <% if @user.api_token %>
41
  <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
41
    <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %><br />
42
    <% if @user.api_token.last_used_on %>
43
      <%= l(:label_api_access_key_last_used_on, distance_of_time_in_words(Time.now, @user.api_token.last_used_on)) %>
44
    <% else %>
45
      <%= l(:label_api_access_key_never_used) %>
46
    <% end %>
42 47
  <% else %>
43
  <%= l(:label_missing_api_access_key) %>
48
    <%= l(:label_missing_api_access_key) %>
44 49
  <% end %>
45 50
  (<%= link_to l(:button_reset), my_api_key_path, :method => :post %>)
46 51
  </p>
config/locales/de.yml
434 434
  label_any_issues_not_in_project: irgendein Ticket nicht im Projekt
435 435
  label_api_access_key: API-Zugriffsschlüssel
436 436
  label_api_access_key_created_on: Der API-Zugriffsschlüssel wurde vor %{value} erstellt
437
  label_api_access_key_last_used_on: "Zuletzt verwendet: vor %{value}"
438
  label_api_access_key_never_used: Nie verwendet
437 439
  label_applied_status: Zugewiesener Status
438 440
  label_ascending: Aufsteigend
439 441
  label_ask: Nachfragen
config/locales/en.yml
1035 1035
  label_api_access_key: API access key
1036 1036
  label_missing_api_access_key: Missing an API access key
1037 1037
  label_api_access_key_created_on: "API access key created %{value} ago"
1038
  label_api_access_key_last_used_on: "Last used: %{value} ago"
1039
  label_api_access_key_never_used: Never used
1038 1040
  label_profile: Profile
1039 1041
  label_subtask: Subtask
1040 1042
  label_subtask_plural: Subtasks
config/locales/fr.yml
921 921
  label_api_access_key: Clé d'accès API
922 922
  label_missing_api_access_key: Clé d'accès API manquante
923 923
  label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
924
  label_api_access_key_last_used_on: "Dernier usage : il y a %{value}"
925
  label_api_access_key_never_used: Jamais utilisée
924 926
  label_profile: Profil
925 927
  label_subtask_plural: Sous-tâches
926 928
  label_project_copy_notifications: Envoyer les notifications durant la copie du projet
config/locales/ja.yml
806 806
  label_api_access_key: APIアクセスキー
807 807
  label_missing_api_access_key: APIアクセスキーが見つかりません
808 808
  label_api_access_key_created_on: "APIアクセスキーは%{value}前に作成されました"
809
  label_api_access_key_last_used_on: "最終使用:%{value}前"
810
  label_api_access_key_never_used: 未使用
809 811
  label_subtask_plural: 子チケット
810 812
  label_project_copy_notifications: コピーしたチケットのメール通知を送信する
811 813
  label_principal_search: "ユーザーまたはグループの検索:"
test/integration/api_test/authentication_test.rb
161 161
    assert_response :success
162 162
    assert_select 'h2', :text => "#{user.initials} #{user.name}"
163 163
  end
164

  
165
  def test_api_key_usage_via_header_should_update_last_used_on
166
    user = User.generate!
167
    token = Token.create!(:user => user, :action => 'api')
168
    assert_nil token.last_used_on
169
    get '/users/current.xml', :headers => {'X-Redmine-API-Key' => token.value}
170
    assert_response :ok
171
    assert_not_nil token.reload.last_used_on
172
  end
173

  
174
  def test_api_key_usage_via_parameter_should_update_last_used_on
175
    user = User.generate!
176
    token = Token.create!(:user => user, :action => 'api')
177
    assert_nil token.last_used_on
178
    get "/users/current.xml?key=#{token.value}"
179
    assert_response :ok
180
    assert_not_nil token.reload.last_used_on
181
  end
182

  
183
  def test_api_key_usage_via_basic_auth_should_update_last_used_on
184
    user = User.generate!
185
    token = Token.create!(:user => user, :action => 'api')
186
    assert_nil token.last_used_on
187
    get '/users/current.xml', :headers => credentials(token.value, 'X')
188
    assert_response :ok
189
    assert_not_nil token.reload.last_used_on
190
  end
191

  
192
  def test_failed_api_auth_should_not_update_last_used_on
193
    user = User.generate!
194
    token = Token.create!(:user => user, :action => 'api')
195
    get '/users/current.xml', :headers => {'X-Redmine-API-Key' => 'wrong_key'}
196
    assert_response :unauthorized
197
    assert_nil token.reload.last_used_on
198
  end
164 199
end
test/unit/token_test.rb
137 137
    token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
138 138
    assert_nil Token.find_token('api', token.value, 1)
139 139
  end
140

  
141
  def test_touch_api_token_should_update_last_used_on_when_never_used
142
    token = Token.create!(:user_id => 1, :action => 'api')
143
    assert_nil token.last_used_on
144
    Token.touch_api_token(token.value)
145
    assert_not_nil token.reload.last_used_on
146
  end
147

  
148
  def test_touch_api_token_should_update_last_used_on_when_older_than_one_minute
149
    token = Token.create!(:user_id => 1, :action => 'api', :last_used_on => 2.minutes.ago)
150
    last_used = token.last_used_on
151
    Token.touch_api_token(token.value)
152
    assert token.reload.last_used_on > last_used
153
  end
154

  
155
  def test_touch_api_token_should_not_update_last_used_on_within_one_minute
156
    token = Token.create!(:user_id => 1, :action => 'api', :last_used_on => 1.second.ago)
157
    last_used = token.reload.last_used_on
158
    Token.touch_api_token(token.value)
159
    assert_equal last_used.to_i, token.reload.last_used_on.to_i
160
  end
161

  
162
  def test_touch_api_token_should_not_affect_other_action_tokens
163
    token = Token.create!(:user_id => 1, :action => 'feeds')
164
    Token.touch_api_token(token.value)
165
    assert_nil token.reload.last_used_on
166
  end
140 167
end
db/migrate/20260409000000_add_last_used_on_to_tokens.rb
1
# frozen_string_literal: true
2

  
3
class AddLastUsedOnToTokens < ActiveRecord::Migration[8.1]
4
  def change
5
    add_column :tokens, :last_used_on, :datetime
6
  end
7
end
    (1-1/1)