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.
This commit is contained in:
evazion
2021-02-14 20:54:45 -06:00
parent 25fda1ecc2
commit d99985160a
4 changed files with 39 additions and 3 deletions

View File

@@ -3,6 +3,8 @@
### API Changes ### API Changes
* You can now have multiple API keys. * 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 * API keys can be restricted to only work with certain IPs or certain API
endpoints. endpoints.
* If you're an app or script developer, and you have an app that requests API * If you're an app or script developer, and you have an app that requests API

View File

@@ -90,6 +90,7 @@ class SessionLoader
def authenticate_api_key(name, key) def authenticate_api_key(name, key)
user, api_key = User.find_by_name(name)&.authenticate_api_key(key) user, api_key = User.find_by_name(name)&.authenticate_api_key(key)
raise AuthenticationFailure if user.blank? 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]) raise User::PrivilegeError if !api_key.has_permission?(request.remote_ip, request.params[:controller], request.params[:action])
CurrentUser.user = user CurrentUser.user = user
end end
@@ -117,6 +118,11 @@ class SessionLoader
CurrentUser.user.update_attribute(:last_ip_addr, @request.remote_ip) CurrentUser.user.update_attribute(:last_ip_addr, @request.remote_ip)
end 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 def set_time_zone
Time.zone = CurrentUser.user.time_zone Time.zone = CurrentUser.user.time_zone
end end

View File

@@ -51,14 +51,22 @@
<%= safe_join(api_key.permitted_ip_addresses, "<br>".html_safe).presence || "All" %> <%= safe_join(api_key.permitted_ip_addresses, "<br>".html_safe).presence || "All" %>
<% end %> <% end %>
<% if !params[:user_id].present? %> <% t.column :uses %>
<% t.column "User" do |api_key| %>
<%= link_to_user api_key.user %> <% t.column "Last Used" do |api_key| %>
<%= time_ago_in_words_tagged api_key.last_used_at %>
<% if api_key.last_ip_address.present? %>
<br>by <%= api_key.last_ip_address %>
<% end %> <% end %>
<% end %> <% end %>
<% t.column "Created" do |api_key| %> <% t.column "Created" do |api_key| %>
<%= time_ago_in_words_tagged api_key.created_at %> <%= time_ago_in_words_tagged api_key.created_at %>
<% if !params[:user_id].present? %>
<br> by <%= link_to_user api_key.user %>
<% end %>
<% end %> <% end %>
<% t.column column: "control" do |api_key| %> <% t.column column: "control" do |api_key| %>

View File

@@ -52,14 +52,20 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
should "succeed for api key matches" do should "succeed for api key matches" do
basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}" basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}"
get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string } get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string }
assert_response :success assert_response :success
assert_equal(1, @api_key.reload.uses)
assert_not_nil(@api_key.reload.last_used_at)
end end
should "succeed when the user has multiple api keys" do should "succeed when the user has multiple api keys" do
@api_key2 = create(:api_key, user: @user) @api_key2 = create(:api_key, user: @user)
basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key2.key}")}" basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key2.key}")}"
get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string } get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string }
assert_response :success assert_response :success
assert_equal(1, @api_key2.reload.uses)
assert_not_nil(@api_key2.reload.last_used_at)
end end
should "fail for api key mismatches" do should "fail for api key mismatches" do
@@ -80,13 +86,19 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
context "using the api_key parameter" do context "using the api_key parameter" do
should "succeed for api key matches" do should "succeed for api key matches" do
get edit_user_path(@user), params: { login: @user.name, api_key: @api_key.key } get edit_user_path(@user), params: { login: @user.name, api_key: @api_key.key }
assert_response :success assert_response :success
assert_equal(1, @api_key.reload.uses)
assert_not_nil(@api_key.reload.last_used_at)
end end
should "succeed when the user has multiple api keys" do should "succeed when the user has multiple api keys" do
@api_key2 = create(:api_key, user: @user) @api_key2 = create(:api_key, user: @user)
get edit_user_path(@user), params: { login: @user.name, api_key: @api_key2.key } get edit_user_path(@user), params: { login: @user.name, api_key: @api_key2.key }
assert_response :success assert_response :success
assert_equal(1, @api_key2.reload.uses)
assert_not_nil(@api_key2.reload.last_used_at)
end end
should "fail for api key mismatches" do 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") 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 } get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key }
assert_response 403 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 end
should "restrict requests to the permitted endpoints" do 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" }} put post_path(@post), params: { login: @api_key.user.name, api_key: @api_key.key, post: { rating: "s" }}
assert_response 403 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
end end