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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,14 +51,22 @@
|
||||
<%= safe_join(api_key.permitted_ip_addresses, "<br>".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? %>
|
||||
<br>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? %>
|
||||
<br> by <%= link_to_user api_key.user %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% t.column column: "control" do |api_key| %>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user