Track last usage of API access keys (#43938).

Patch by Vincent Robert (user:Nanego).


git-svn-id: https://svn.redmine.org/redmine/trunk@24576 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Marius Balteanu
2026-04-11 15:38:56 +00:00
parent 368c035c38
commit 81b99bc694
8 changed files with 81 additions and 5 deletions

View File

@@ -94,9 +94,15 @@ class Token < ApplicationRecord
# Returns the active user who owns the key for the given action
def self.find_active_user(action, key, validity_days=nil)
user = find_user(action, key, validity_days)
if user && user.active?
user
token = find_token(action, key, validity_days)
if token
user = token.user
if user&.active?
if token.updated_on.nil? || token.updated_on <= 1.minute.ago
token.update_column(:updated_on, Time.now.utc)
end
user
end
end
end

View File

@@ -38,9 +38,14 @@
<%= javascript_tag("$('#api-access-key').hide();") %>
<p>
<% if @user.api_token %>
<%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
<%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %><br />
<% if @user.api_token.updated_on > @user.api_token.created_on %>
<%= l(:label_api_access_key_last_used_on, distance_of_time_in_words(Time.now, @user.api_token.updated_on)) %>
<% else %>
<%= l(:label_api_access_key_never_used) %>
<% end %>
<% else %>
<%= l(:label_missing_api_access_key) %>
<%= l(:label_missing_api_access_key) %>
<% end %>
(<%= link_to l(:button_reset), my_api_key_path, :method => :post %>)
</p>

View File

@@ -434,6 +434,8 @@ de:
label_any_issues_not_in_project: irgendein Ticket nicht im Projekt
label_api_access_key: API-Zugriffsschlüssel
label_api_access_key_created_on: Der API-Zugriffsschlüssel wurde vor %{value} erstellt
label_api_access_key_last_used_on: "Zuletzt verwendet: vor %{value}"
label_api_access_key_never_used: Nie verwendet
label_applied_status: Zugewiesener Status
label_ascending: Aufsteigend
label_ask: Nachfragen

View File

@@ -1035,6 +1035,8 @@ en:
label_api_access_key: API access key
label_missing_api_access_key: Missing an API access key
label_api_access_key_created_on: "API access key created %{value} ago"
label_api_access_key_last_used_on: "Last used: %{value} ago"
label_api_access_key_never_used: Never used
label_profile: Profile
label_subtask: Subtask
label_subtask_plural: Subtasks

View File

@@ -921,6 +921,8 @@ fr:
label_api_access_key: Clé d'accès API
label_missing_api_access_key: Clé d'accès API manquante
label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
label_api_access_key_last_used_on: "Dernier usage : il y a %{value}"
label_api_access_key_never_used: Jamais utilisée
label_profile: Profil
label_subtask_plural: Sous-tâches
label_project_copy_notifications: Envoyer les notifications durant la copie du projet

View File

@@ -806,6 +806,8 @@ ja:
label_api_access_key: APIアクセスキー
label_missing_api_access_key: APIアクセスキーが見つかりません
label_api_access_key_created_on: "APIアクセスキーは%{value}前に作成されました"
label_api_access_key_last_used_on: "最終使用:%{value}前"
label_api_access_key_never_used: 未使用
label_subtask_plural: 子チケット
label_project_copy_notifications: コピーしたチケットのメール通知を送信する
label_principal_search: "ユーザーまたはグループの検索:"

View File

@@ -161,4 +161,40 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base
assert_response :success
assert_select 'h2', :text => "#{user.initials} #{user.name}"
end
def test_api_key_usage_via_header_should_update_updated_on
user = User.generate!
token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
updated = token.updated_on
get '/users/current.xml', :headers => {'X-Redmine-API-Key' => token.value}
assert_response :ok
assert token.reload.updated_on > updated
end
def test_api_key_usage_via_parameter_should_update_updated_on
user = User.generate!
token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
updated = token.updated_on
get "/users/current.xml?key=#{token.value}"
assert_response :ok
assert token.reload.updated_on > updated
end
def test_api_key_usage_via_basic_auth_should_update_updated_on
user = User.generate!
token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
updated = token.updated_on
get '/users/current.xml', :headers => credentials(token.value, 'X')
assert_response :ok
assert token.reload.updated_on > updated
end
def test_failed_api_auth_should_not_update_updated_on
user = User.generate!
token = Token.create!(:user => user, :action => 'api', :updated_on => 2.minutes.ago)
updated = token.updated_on
get '/users/current.xml', :headers => {'X-Redmine-API-Key' => 'wrong_key'}
assert_response :unauthorized
assert_equal updated.to_i, token.reload.updated_on.to_i
end
end

View File

@@ -137,4 +137,25 @@ class TokenTest < ActiveSupport::TestCase
token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
assert_nil Token.find_token('api', token.value, 1)
end
def test_find_active_user_should_bump_updated_on_when_not_recently_updated
token = Token.create!(:user_id => 1, :action => 'api', :updated_on => 2.minutes.ago)
updated = token.updated_on
Token.find_active_user('api', token.value)
assert token.reload.updated_on > updated
end
def test_find_active_user_should_not_bump_updated_on_within_one_minute
token = Token.create!(:user_id => 1, :action => 'api', :updated_on => 1.second.ago)
updated = token.reload.updated_on
Token.find_active_user('api', token.value)
assert_equal updated.to_i, token.reload.updated_on.to_i
end
def test_find_active_user_should_bump_updated_on_for_feeds_token
token = Token.create!(:user_id => 1, :action => 'feeds', :updated_on => 2.minutes.ago)
updated = token.updated_on
Token.find_active_user('feeds', token.value)
assert token.reload.updated_on > updated
end
end