Cap last_used_ips to the 5 most recent when rendered

What does this MR do and why?

The last_used_ips field is documented as "the five most recent unique IP addresses", but both render paths returned every stored row: the REST entity (PersonalAccessTokenWithLastUsedIps) and the GraphQL PersonalAccessToken type. Because the write-path trim in PersonalAccessTokens::LastUsedService is unreliable, about 14% of tokens hold more than five recorded IPs, so those responses returned the full list.

Separately, neither path de-duplicated. The table has no unique index on (token, ip_address), and production holds 66,876 duplicate pairs out of about 10.26 million distinct pairs (about 0.65%).

This extracts the shared render logic into PersonalAccessToken#recent_last_used_ips, called by both the REST entity and the GraphQL type. It sorts by created_at, de-duplicates by IP keeping the newest occurrence, and caps the result at five, reusing PersonalAccessTokens::LastUsedService::NUM_IPS_TO_STORE so the read cap always tracks the write cap. It runs in Ruby on the already-loaded association, so it adds no query and does not reintroduce the N+1 that the preloading work removed.

The de-duplication is defense-in-depth for the documented "unique" contract, applied at render time regardless of what is already stored. The stored duplicate and excess rows are handled separately by step 2 (write-path trim, !250690 (merged)) and step 3 (batched background migration, !250697 (merged)), both of which also de-duplicate.

The GraphQL field is not behind the expose_last_used_ips_for_access_tokens feature flag, so this is a user-facing fix (Changelog: fixed). The REST field is behind that (disabled) flag.

This is step 1 of #616954 (closed).

Known trade-off: the render logic now lives on the PersonalAccessToken model as a single shared method for both paths, but the cap constant (NUM_IPS_TO_STORE) still lives on LastUsedService, a write-path service, and is referenced from the model rather than moved. This is deliberate, to avoid conflicting with the step 2 MR that edits that service.

How to set up and validate locally

  1. Record more than five IPs for a token, in the Rails console:

    t = PersonalAccessToken.last
    6.times { |i| t.last_used_ips.create!(organization: t.organization, ip_address: "192.0.2.#{i}") }
  2. GraphQL: query currentUser { personalAccessTokens { nodes { lastUsedIps } } } and confirm at most five IPs are returned (the most recent).

  3. REST (with expose_last_used_ips_for_access_tokens enabled): GET /api/v4/personal_access_tokens/:id and confirm at most five.

  4. De-duplication: record the same ip_address several times for a token, then confirm the rendered lastUsedIps (GraphQL or REST) shows that IP only once.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Eduardo Sanz García

Merge request reports

Loading
Loading