Knowledge Graph AuthZ Rails Redaction Endpoint
Implement a reusable batch authorization service (Authz::RedactionService) that checks whether a user has read access to a collection of resources. This service is the foundation for Layer 3: Final Redaction Layer in the Knowledge Graph security architecture.
Background
Knowledge Graph Security Architecture
The GitLab Knowledge Graph (GKG) service provides intelligent search and discovery across SDLC data stored in ClickHouse. The security architecture consists of three layers:
- Layer 1: Query-time Filtering - ClickHouse queries use traversal IDs to filter results at the group/project level
- Layer 2: Pre-filtering - Additional filtering based on user's project authorizations
-
Layer 3: Final Redaction - Rails-based authorization using
Ability.allowed?for fine-grained access control
This issue focuses on Layer 3, which is necessary because traversal ID filtering cannot account for:
- Confidential issues - Only visible to project members and issue participants
- Runtime checks - SAML group links, IP restrictions
- Custom roles - Fine-grained permissions beyond Reporter+ access
- Resource-specific visibility - Feature access levels, banned/blocked users
Why a Separate Service?
While this service is being built for Knowledge Graph, it's designed to be generic and reusable:
- Follows the same pattern as
SearchService#visible_result? - Can be used by any feature requiring batch authorization checks
- Placed in CE (
Authz::namespace) for broader availability - EE extension adds support for EE-specific resources (epics, vulnerabilities)
Requirements
Functional Requirements
- Accept a user and a hash of resources grouped by type
- Support resource types: issues, merge_requests, projects, milestones, snippets (CE) + epics, vulnerabilities (EE)
- Return a hash mapping each resource ID to a boolean authorization result
- Use GitLab's standard
Ability.allowed?for authorization decisions - Accept an optional logger parameter for audit logging
Non-Functional Requirements
- Performance: Prevent N+1 queries through association preloading
- Consistency: Follow the same authorization pattern as SearchService
- Scalability: Support batch sizes up to 500 resources per request
Technical Design
Service Interface
# CE: app/services/authz/redaction_service.rb
service = Authz::RedactionService.new(
user: current_user,
resources_by_type: {
'issues' => [123, 456],
'merge_requests' => [789]
},
logger: optional_logger
)
result = service.execute
# => {
# 'issues' => { 123 => true, 456 => false },
# 'merge_requests' => { 789 => true }
# }
N+1 Prevention Strategy
The service uses association preloading to prevent N+1 queries during policy evaluation:
PRELOAD_ASSOCIATIONS = {
'issues' => [
{ project: [:namespace, :project_feature, :group] },
:author,
:work_item_type
],
'merge_requests' => [
{ target_project: [:namespace, :project_feature, :group] },
:author
],
'projects' => [:namespace, :project_feature, :group],
'milestones' => [{ project: [:namespace, :project_feature] }, :group],
'snippets' => [{ project: [:namespace, :project_feature] }, :author]
}
How it works:
Without preloading, checking 100 issues would trigger 300+ queries:
- 1 query to load issues
- For each issue: queries for project, namespace, project_feature, etc.
With preloading, the same operation uses ~4 queries regardless of batch size:
- 1 query for issues with JOINs/eager loading for all associations
- Policy evaluation uses already-loaded data
DeclarativePolicy Optimization
The service wraps authorization checks in DeclarativePolicy.user_scope:
DeclarativePolicy.user_scope do
resources_by_type.each_with_object({}) do |(type, ids), results|
results[type] = authorize_resources_of_type(type, ids, loaded_resources)
end
end
This caches policy evaluations for the same user across multiple resources, following the pattern from Ability.issues_readable_by_user.
Authorization Pattern
Uses the same visible_result? pattern as SearchService:
def visible_result?(resource)
return true unless resource.respond_to?(:to_ability_name) && DeclarativePolicy.has_policy?(resource)
Ability.allowed?(user, :"read_#{resource.to_ability_name}", resource)
end
Assumptions
- User is already authenticated: The service does NOT validate user state (blocked, deactivated). The caller is responsible for ensuring the user is valid.
-
Resources exist: Non-existent resource IDs return
false(denied) - Batch size is reasonable: The API endpoint (separate issue) will enforce max batch size