From d99985160aed2c37bffa8d479f59eb39833d2077 Mon Sep 17 00:00:00 2001 From: evazion Date: Sun, 14 Feb 2021 20:54:45 -0600 Subject: [PATCH] api keys: add API key usage tracking. Track when an API key was last used, which IP address last used it, and how many times it's been used overall. This is so you can tell when an API key was last used, so you know if the key is safe to delete, and so you can tell if an unrecognized IP has used your key. --- CHANGELOG.md | 2 ++ app/logical/session_loader.rb | 6 ++++++ app/views/api_keys/index.html.erb | 14 ++++++++++--- .../functional/application_controller_test.rb | 20 +++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) 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