Fix KG auth with Short Lived Cache for GroupsFinder and ProjectsFinder
Summary
Follow-up to merged MR !231605 (merged). Fixes an authorization regression where users whose only reporter+ access to a KG-enabled namespace comes through a group_group_link received HTTP 403 on the Orbit API, even though their JWT included the namespace's traversal prefix.
Also picks up John's Redis cache for Search::GroupsFinder / Search::ProjectsFinder (!224795 (closed)), relocates the cache-busting worker to the Search namespace, and expands the event-store subscriptions so finder caches stay consistent with every membership change path.
Part of #591202 (closed)
Prior art !224372 (merged)
What changed
- Gate uses
Search::GroupsFinder(same as the JWT) sogroup_group_linkaccess is honored AuthorizationContextrunsGroupsFinderonce per request; gate + JWT share the resultAuthorizationContextno longer keeps its ownRails.cache— finder owns the cache- Search finders cache-bust by bumping a per-user version, covering every
min_access_level/featurescombo Rails.cache.write_multibatches invalidation (one pipelined Redis call per finder)- Worker renamed
Analytics::KnowledgeGraph::ExpireTraversalIdCacheWorker→Search::ExpireFinderCacheWorkerand moved toSearchSubscriptions - Subscribes to
Members::UpdatedEvent,Members::AcceptedInviteEvent,Members::MembershipModifiedByAdminEventin addition to the previous four
How to reproduce and validate locally
Setup
# Rails console
admin = User.find(1)
root_enabled = Group.find_or_create_by!(path: 'kg-enabled') { |g| g.name = 'KG Enabled'; g.organization_id = admin.organization_id }
other_group = Group.find_or_create_by!(path: 'kg-shared') { |g| g.name = 'KG Shared'; g.organization_id = admin.organization_id }
Analytics::KnowledgeGraph::EnabledNamespace.find_or_create_by!(root_namespace_id: root_enabled.id)
user = User.find_or_create_by!(username: 'kg-linked') do |u|
u.email = 'kg-linked@example.com'
u.name = 'KG Linked'
u.password = SecureRandom.hex(12)
u.confirmed_at = Time.current
u.organization_id = admin.organization_id
end
Organizations::OrganizationUser.find_or_create_by!(user_id: user.id, organization_id: admin.organization_id)
# Share the KG-enabled group with the other group at REPORTER level,
# then give the user reporter on the other (non-KG-enabled) group.
GroupGroupLink.find_or_create_by!(shared_group: root_enabled, shared_with_group: other_group) do |link|
link.group_access = Gitlab::Access::REPORTER
end
Member.where(user_id: user.id).delete_all
other_group.add_reporter(user)
Users::RefreshAuthorizedProjectsService.new(user).executeReproduce the regression (before this MR)
ctx = Analytics::KnowledgeGraph::AuthorizationContext.new(user)
ctx.reporter_plus_traversal_ids[:group_traversal_ids]
#=> ["1/<root_enabled.id>/", "1/<other_group.id>/"] <-- JWT sees the enabled namespace
ctx.has_enabled_namespaces?
#=> false <-- gate blocks the userPOST to /api/v4/orbit/query with a PAT from that user and it returns 403 No Knowledge Graph enabled namespaces available.
Validate the fix (with this MR)
ctx = Analytics::KnowledgeGraph::AuthorizationContext.new(user)
ctx.has_enabled_namespaces? #=> trueThen:
TOKEN=<user PAT>
curl -sk -H "PRIVATE-TOKEN: $TOKEN" -H 'Content-Type: application/json' \
-X POST "https://gdk.test:3443/api/v4/orbit/query" \
-d '{"query":{"query_type":"search","node":{"id":"g","entity":"Group","columns":["full_path"]},"limit":20}}'Returns HTTP 200 with the rows the user reaches through the link.
Validate the share-level boundary
link = GroupGroupLink.find_by(shared_group: root_enabled, shared_with_group: other_group)
link.update!(group_access: Gitlab::Access::GUEST)
Users::RefreshAuthorizedProjectsService.new(user).execute
Analytics::KnowledgeGraph::AuthorizationContext.new(user.reload).has_enabled_namespaces?
#=> false <-- guest-level shares do not passCache invalidation smoke test
user = User.find(1)
k_before = Search::GroupsFinder.redis_cache_key(user.id, min_access_level: Gitlab::Access::REPORTER)
Search::GroupsFinder.expire_cache_for_users([user.id])
k_after = Search::GroupsFinder.redis_cache_key(user.id, min_access_level: Gitlab::Access::REPORTER)
k_before != k_after #=> true (version bumped, every variant orphaned)Tests
ee/spec/lib/analytics/knowledge_graph/authorization_context_spec.rbcovers subgroup access,group_group_linkat REPORTER (passes) and GUEST (blocks), mixed enabled/disabled access, and the existing boundary cases.ee/spec/workers/search/expire_finder_cache_worker_spec.rbcovers the rename, subscriptions to all seven events (four existing plus the three new ones),member_user_idextraction, and thewrite_multiinvalidation path.ee/spec/finders/search/{groups,projects}_finder_spec.rbwere updated to build keys throughredis_cache_keyso they stay in sync with the version scheme.
Notes
search_finders_redis_cacheremainsdefault_enabled: false. Rollout is owned by Global Search.- No worker-rename compatibility shim is needed because the previous class was only subscribed behind the disabled
knowledge_graph_infraflag.