diff options
author | Luke Duncalfe <lduncalfe@eml.cc> | 2019-03-04 15:30:32 +1300 |
---|---|---|
committer | Luke Duncalfe <lduncalfe@eml.cc> | 2019-04-03 14:36:33 +1300 |
commit | 8207f7877fea6987cbd8ef26e6f01feca6608bd2 (patch) | |
tree | 971a61fa9885702ef753bf8fde5e87ed0d531913 /lib | |
parent | 3d24e7225ea01d5a4f8398b7626eee77a904b8dc (diff) | |
download | gitlab-ce-8207f7877fea6987cbd8ef26e6f01feca6608bd2.tar.gz |
GraphQL Type authorization
Enables authorizations to be defined on GraphQL Types.
module Types
class ProjectType < BaseObject
authorize :read_project
end
end
If a field has authorizations defined on it, and the return type of the
field also has authorizations defined on it. then all of the combined
permissions in the authorizations will be checked and must pass.
Connection fields are checked by "digging" to find the type class of the
"node" field in the expected location of edges->node.
Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/54417
Diffstat (limited to 'lib')
-rw-r--r-- | lib/gitlab/graphql/authorize/authorize_field_service.rb | 94 | ||||
-rw-r--r-- | lib/gitlab/graphql/authorize/instrumentation.rb | 44 | ||||
-rw-r--r-- | lib/gitlab/graphql/errors.rb | 1 |
3 files changed, 100 insertions, 39 deletions
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb new file mode 100644 index 00000000000..f3ca82ec697 --- /dev/null +++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Authorize + class AuthorizeFieldService + def initialize(field) + @field = field + @old_resolve_proc = @field.resolve_proc + end + + def authorizations? + authorizations.present? + end + + def authorized_resolve + proc do |obj, args, ctx| + resolved_obj = @old_resolve_proc.call(obj, args, ctx) + checker = build_checker(ctx[:current_user]) + + if resolved_obj.respond_to?(:then) + resolved_obj.then(&checker) + else + checker.call(resolved_obj) + end + end + end + + private + + def authorizations + @authorizations ||= (type_authorizations + field_authorizations).uniq + end + + # Returns any authorize metadata from the return type of @field + def type_authorizations + type = @field.type + + # When the return type of @field is a collection, find the singular type + if type.get_field('edges') + type = node_type_for_relay_connection(type) + elsif type.list? + type = node_type_for_basic_connection(type) + end + + Array.wrap(type.metadata[:authorize]) + end + + # Returns any authorize metadata from @field + def field_authorizations + Array.wrap(@field.metadata[:authorize]) + end + + def build_checker(current_user) + lambda do |value| + # Load the elements if they were not loaded by BatchLoader yet + value = value.sync if value.respond_to?(:sync) + + check = lambda do |object| + authorizations.all? do |ability| + Ability.allowed?(current_user, ability, object) + end + end + + case value + when Array, ActiveRecord::Relation + value.select(&check) + else + value if check.call(value) + end + end + end + + # Returns the singular type for relay connections. + # This will be the type class of edges.node + def node_type_for_relay_connection(type) + type = type.get_field('edges').type.unwrap.get_field('node')&.type + + if type.nil? + raise Gitlab::Graphql::Errors::ConnectionDefinitionError, + 'Connection Type must conform to the Relay Cursor Connections Specification' + end + + type + end + + # Returns the singular type for basic connections, for example `[Types::ProjectType]` + def node_type_for_basic_connection(type) + type.unwrap + end + end + end + end +end diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb index 593da8471dd..15ecc3b04f0 100644 --- a/lib/gitlab/graphql/authorize/instrumentation.rb +++ b/lib/gitlab/graphql/authorize/instrumentation.rb @@ -7,46 +7,12 @@ module Gitlab # Replace the resolver for the field with one that will only return the # resolved object if the permissions check is successful. def instrument(_type, field) - required_permissions = Array.wrap(field.metadata[:authorize]) - return field if required_permissions.empty? + service = AuthorizeFieldService.new(field) - old_resolver = field.resolve_proc - - new_resolver = -> (obj, args, ctx) do - resolved_obj = old_resolver.call(obj, args, ctx) - checker = build_checker(ctx[:current_user], required_permissions) - - if resolved_obj.respond_to?(:then) - resolved_obj.then(&checker) - else - checker.call(resolved_obj) - end - end - - field.redefine do - resolve(new_resolver) - end - end - - private - - def build_checker(current_user, abilities) - lambda do |value| - # Load the elements if they weren't loaded by BatchLoader yet - value = value.sync if value.respond_to?(:sync) - - check = lambda do |object| - abilities.all? do |ability| - Ability.allowed?(current_user, ability, object) - end - end - - case value - when Array - value.select(&check) - else - value if check.call(value) - end + if service.authorizations? + field.redefine { resolve(service.authorized_resolve) } + else + field end end end diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb index fe74549e322..bcbba72e017 100644 --- a/lib/gitlab/graphql/errors.rb +++ b/lib/gitlab/graphql/errors.rb @@ -6,6 +6,7 @@ module Gitlab BaseError = Class.new(GraphQL::ExecutionError) ArgumentError = Class.new(BaseError) ResourceNotAvailable = Class.new(BaseError) + ConnectionDefinitionError = Class.new(BaseError) end end end |