Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api_key): cache api key and organization #3137

Merged
merged 1 commit into from
Feb 10, 2025
Merged

Conversation

vincent-pochet
Copy link
Collaborator

@vincent-pochet vincent-pochet commented Feb 4, 2025

Context

This PR is part of a global initiative to improve the performance of the LAGO Rest API.

Today every request made received by the API leads to at least two database queries to authenticate the organization. A first one is fetching the api_key using the provided bearer token from the HTTP header, the second one loads the related organization.

As by nature, api_keys and organizations are not update really often, adding a caching strategy could reduce drastically the time consumed authenticating the organization.

Description

This PR changes the authentication mechanism by:

  • Adding a dedicated cache service for API key. It default to no cache unless cache is explicitly turned on.
  • If cache is on:
    • Checking for cached api_key and organization
    • Fetching the values from the DB is no cache is present
    • Caching the values
    • Handling cache expiration when updating api keys and organization

@vincent-pochet vincent-pochet force-pushed the speed-up-api-key branch 6 times, most recently from 5ad4aa9 to 38cec71 Compare February 4, 2025 16:35
@vincent-pochet vincent-pochet force-pushed the speed-up-api-key branch 4 times, most recently from 9905414 to 19be581 Compare February 6, 2025 15:20
@vincent-pochet vincent-pochet marked this pull request as ready for review February 6, 2025 15:21
@julienbourdeau
Copy link
Contributor

I understand the point but I believe loading something as core as current_organization from a json cached in a different database isn't worth it. The organization is used everywhere.
Seen current_organization.reload in serializer and other scenario spec failing feels wrong to me.

I think a good first step would be caching the key for a small period of time. Under "heavy load" it would already save hundreds of db queries to find the key, while not adding much risk.

I'd also not try to load ActiveModel from a serialize json. We barely used the current_api_key so I'd turn it to a hash instead. This would avoid the sort of things: ApiKey.new(data["api_key"].slice(*ApiKey.column_names))

The cache is refreshed very often in case something changed, no need to expire cache manually.

It's not going to save as many DB queries as your approach but I believe the effort and the risk are so much lower.

def authenticate
  return unauthorized_error unless auth_token

  @current_api_key = Rails.cache.fetch("key-#{auth_token}", expire_in: 2.minutes) do
    key = ApiKey.find_by(value: auth_token)

    {
      'id' => key.id,
      'permissions' => key.permissions,
      'expires_at' => key.expires_at,
      'organization_id' => key.organization_id,
    }
  end

  return unauthorized_error unless current_api_key['id']
  
  # Check expiration or simply admit that key expiration are delayed 2 minutes

  @current_organization = Organization.find(current_api_key['organization_id'])
  true
end

This is an example snippet, we can still expire cache when the key is rotated or deleted. Or worst case scenario, it's delayed 2 minutes.

  def authorize
     # get rid of ApiKey.permit? and use THE SAME way to check permissions
-    return if current_api_key.permit?(resource_name, mode)
+    return if Array(current_api_key.dig('permissions', resource_name)), mode)

    forbidden_error(code: "#{mode}_action_not_allowed_for_#{resource_name}")
  end

Updating other usage of current_api_key is pretty straight forward.

def track_api_key_usage
  return unless track_api_key_usage?

  Rails.cache.write(
-    "api_key_last_used_#{current_api_key.id}",
+    "api_key_last_used_#{current_api_key['id']}",
    Time.current.iso8601
  )
end

@vincent-pochet vincent-pochet force-pushed the speed-up-api-key branch 6 times, most recently from 3372d63 to 47c962c Compare February 7, 2025 11:43
@vincent-pochet vincent-pochet changed the title POC(api_key): cache api key and organization feat(api_key): cache api key and organization Feb 7, 2025
@vincent-pochet vincent-pochet force-pushed the speed-up-api-key branch 2 times, most recently from 3e88fcd to 1b87ace Compare February 10, 2025 09:53
Copy link
Contributor

@julienbourdeau julienbourdeau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love that it can be enabled/disabled per endpoint now 👌

@vincent-pochet vincent-pochet merged commit 3716074 into main Feb 10, 2025
6 checks passed
@vincent-pochet vincent-pochet deleted the speed-up-api-key branch February 10, 2025 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants