Feature #43938 » 0001-track-api-key-last-used-on.patch
| 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 |
|