From 81b99bc694f1620798d1643cd4b19a69ff568ee3 Mon Sep 17 00:00:00 2001 From: Marius Balteanu Date: Sat, 11 Apr 2026 15:38:56 +0000 Subject: [PATCH] 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 --- app/models/token.rb | 12 +++++-- app/views/my/_sidebar.html.erb | 9 +++-- config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ config/locales/fr.yml | 2 ++ config/locales/ja.yml | 2 ++ .../api_test/authentication_test.rb | 36 +++++++++++++++++++ test/unit/token_test.rb | 21 +++++++++++ 8 files changed, 81 insertions(+), 5 deletions(-) diff --git a/app/models/token.rb b/app/models/token.rb index b98c6dbf3..d425e2b11 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -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 diff --git a/app/views/my/_sidebar.html.erb b/app/views/my/_sidebar.html.erb index 82a22cef2..8bae9ddc0 100644 --- a/app/views/my/_sidebar.html.erb +++ b/app/views/my/_sidebar.html.erb @@ -38,9 +38,14 @@ <%= javascript_tag("$('#api-access-key').hide();") %>

<% 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)) %>
+ <% 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 %>)

diff --git a/config/locales/de.yml b/config/locales/de.yml index 2b3a6042a..69f9ef211 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 90e67bfd8..b97471e90 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 014a47072..74df46e58 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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 diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 51fc2c164..4cfa02716 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -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: "ユーザーまたはグループの検索:" diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb index 4145fb969..b1b28d20c 100644 --- a/test/integration/api_test/authentication_test.rb +++ b/test/integration/api_test/authentication_test.rb @@ -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 diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb index d40b3a2a1..754f0ad04 100644 --- a/test/unit/token_test.rb +++ b/test/unit/token_test.rb @@ -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