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
-
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}") } -
GraphQL: query
currentUser { personalAccessTokens { nodes { lastUsedIps } } }and confirm at most five IPs are returned (the most recent). -
REST (with
expose_last_used_ips_for_access_tokensenabled):GET /api/v4/personal_access_tokens/:idand confirm at most five. -
De-duplication: record the same
ip_addressseveral times for a token, then confirm the renderedlastUsedIps(GraphQL or REST) shows that IP only once.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.