diff --git a/CHANGELOG.md b/CHANGELOG.md index d00c262e4..65af90f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### API Changes * You can now have multiple API keys. +* You can now see when your API keys were last used, how many times they've + been used, and which IP address last used them. * API keys can be restricted to only work with certain IPs or certain API endpoints. * If you're an app or script developer, and you have an app that requests API diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb index 9ae708fc4..57b27dcdc 100644 --- a/app/logical/session_loader.rb +++ b/app/logical/session_loader.rb @@ -90,6 +90,7 @@ class SessionLoader def authenticate_api_key(name, key) user, api_key = User.find_by_name(name)&.authenticate_api_key(key) raise AuthenticationFailure if user.blank? + update_api_key(api_key) raise User::PrivilegeError if !api_key.has_permission?(request.remote_ip, request.params[:controller], request.params[:action]) CurrentUser.user = user end @@ -117,6 +118,11 @@ class SessionLoader CurrentUser.user.update_attribute(:last_ip_addr, @request.remote_ip) end + def update_api_key(api_key) + api_key.increment!(:uses, touch: :last_used_at) + api_key.update!(last_ip_address: request.remote_ip) + end + def set_time_zone Time.zone = CurrentUser.user.time_zone end diff --git a/app/views/api_keys/index.html.erb b/app/views/api_keys/index.html.erb index 2167be418..074209874 100644 --- a/app/views/api_keys/index.html.erb +++ b/app/views/api_keys/index.html.erb @@ -51,14 +51,22 @@ <%= safe_join(api_key.permitted_ip_addresses, "
".html_safe).presence || "All" %> <% end %> - <% if !params[:user_id].present? %> - <% t.column "User" do |api_key| %> - <%= link_to_user api_key.user %> + <% t.column :uses %> + + <% t.column "Last Used" do |api_key| %> + <%= time_ago_in_words_tagged api_key.last_used_at %> + + <% if api_key.last_ip_address.present? %> +
by <%= api_key.last_ip_address %> <% end %> <% end %> <% t.column "Created" do |api_key| %> <%= time_ago_in_words_tagged api_key.created_at %> + + <% if !params[:user_id].present? %> +
by <%= link_to_user api_key.user %> + <% end %> <% end %> <% t.column column: "control" do |api_key| %> diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb index 3c9de79e0..bd96c4a24 100644 --- a/test/functional/application_controller_test.rb +++ b/test/functional/application_controller_test.rb @@ -52,14 +52,20 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest should "succeed for api key matches" do basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}" get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string } + assert_response :success + assert_equal(1, @api_key.reload.uses) + assert_not_nil(@api_key.reload.last_used_at) end should "succeed when the user has multiple api keys" do @api_key2 = create(:api_key, user: @user) basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key2.key}")}" get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string } + assert_response :success + assert_equal(1, @api_key2.reload.uses) + assert_not_nil(@api_key2.reload.last_used_at) end should "fail for api key mismatches" do @@ -80,13 +86,19 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest context "using the api_key parameter" do should "succeed for api key matches" do get edit_user_path(@user), params: { login: @user.name, api_key: @api_key.key } + assert_response :success + assert_equal(1, @api_key.reload.uses) + assert_not_nil(@api_key.reload.last_used_at) end should "succeed when the user has multiple api keys" do @api_key2 = create(:api_key, user: @user) get edit_user_path(@user), params: { login: @user.name, api_key: @api_key2.key } + assert_response :success + assert_equal(1, @api_key2.reload.uses) + assert_not_nil(@api_key2.reload.last_used_at) end should "fail for api key mismatches" do @@ -135,6 +147,10 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("2600:dead:beef::1") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response 403 + + assert_equal(6, @api_key.reload.uses) + assert_equal("2600:dead:beef::1", @api_key.reload.last_ip_address.to_s) + assert_not_nil(@api_key.reload.last_used_at) end should "restrict requests to the permitted endpoints" do @@ -152,6 +168,10 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest put post_path(@post), params: { login: @api_key.user.name, api_key: @api_key.key, post: { rating: "s" }} assert_response 403 + + assert_equal(4, @api_key.reload.uses) + assert_equal("127.0.0.1", @api_key.reload.last_ip_address.to_s) + assert_not_nil(@api_key.reload.last_used_at) end end