Unify authorization pattern in virtual registry GraphQL resolvers
Summary
Currently, virtual registry GraphQL resolvers perform authorization checks in the resolve method instead of using the standard GraphQL authorized? method. We should evaluate whether this pattern should be refactored or if it's semantically correct.
Current Pattern
Most virtual registry resolvers use this pattern:
def resolve(**args)
return unless ::VirtualRegistries::Packages::Maven.virtual_registry_available?(object, current_user)
# resolver logic
end
Examples:
Resolvers::VirtualRegistries::Packages::Maven::UpstreamsResolverTypes::GroupType#maven_virtual_registries
❓ Question
virtual_registry_available? checks multiple conditions:
group.dependency_proxy_feature_available? &&
Feature.enabled?(:maven_virtual_registry, group) &&
group.licensed_feature_available?(:packages_virtual_registry) &&
Ability.allowed?(current_user, permission, group.virtual_registry_policy_subject) &&
VirtualRegistries::Setting.find_for_group(group).enabled
Only Ability.allowed? is actually authorization. The rest are feature availability checks.
However, having permission check Ability.allowed? inside resolve method breaks the convention of GraphQL usage, it's raised in the MR discussion, authorize for the class or authorized? method is the recommend way to do permission check which we don't follow now.
Current solution
In the MR, we made a compromise to align with GraphQL best practices.
We added the authorized? method in the resolver to explicitly show authorization checks (code):
def authorized?(**_args)
current_user&.can?(:admin_virtual_registry, group.virtual_registry_policy_subject)
end
Problem
This creates duplication, we do permission check twice: once in authorized? and again in virtual_registry_available?.
Goals:
- Follow GraphQL best practices using authorize or authorized? for authorization
- Establish a unified authorization pattern across the virtual registries domain
Solution
Continue with the current approach from the MR. We added the authorized? method in the resolver (code):
def authorized?(**_args)
current_user&.can?(:admin_virtual_registry, group.virtual_registry_policy_subject)
end
We need to apply it to all the resolvers in virtual registries scope.
Considered Approaches
Option 1: Split Authorization from Availability
Extract Ability.allowed? from VirtualRegistries::Packages::Maven.virtual_registry_available? since virtual_registry_available? should be a availability check but not permission check.
def authorized?(**args)
Ability.allowed?(current_user, :admin_virtual_registry, object.virtual_registry_policy_subject)
end
def resolve(**args)
# Check feature availability, `Ability.allowed?` part should be removed
return unless VirtualRegistries::Packages::Maven.virtual_registry_available?(..)
# business logic
end
This option will follow the recommend practice, but it also bring some concerns:
- Requires refactoring
virtual_registry_available?which is used in many places (REST APIs, controllers, other GraphQL resolvers) - Every caller would need to add their own permission check, leading to code duplication
Option 2: Put virtual_registry_available? in authorized? method
Follows GraphQL best practices by using the authorized? method for all availability checks. GraphQL Ruby automatically handles authorization failures by returning nil instead of raising errors. However, this approach puts both authorization checks (permissions) and feature availability checks (feature flags, license, settings) in the same method, which may be semantically unclear.
def authorized?(**args)
VirtualRegistries::Packages::Maven.virtual_registry_available?
end
def resolve(**args)
# business logic
end
References
- Related MR: !211283 (merged)