GraphQL integration
You can use Action Policy as an authorization library for your GraphQL Ruby application via the action_policy-graphql gem.
This integration provides the following features:
- Fields & mutations authorization
- List and connections scoping
- Exposing permissions/authorization rules in the API.
Getting Started
First, add the action_policy-graphql gem to your Gemfile (see installation instructions).
Then, include ActionPolicy::GraphQL::Behaviour to your base type (or any other type/mutation where you want to use authorization features):
# For fields authorization, lists scoping and rules exposing
class Types::BaseObject < GraphQL::Schema::Object
include ActionPolicy::GraphQL::Behaviour
end
# For using authorization helpers in mutations
class Types::BaseMutation < GraphQL::Schema::Mutation
include ActionPolicy::GraphQL::Behaviour
end
# For using authorization helpers in resolvers
class Types::BaseResolver < GraphQL::Schema::Resolver
include ActionPolicy::GraphQL::Behaviour
endAuthorization Context
By default, Action Policy uses context[:current_user] as the user authorization context.
NOTE: see below for more information on what's included into ActionPolicy::GraphQL::Behaviour.
Authorizing Fields
You can add authorize: true option to any field (=underlying object) to protect the access (it's equal to calling authorize! object, to: :show?):
# authorization could be useful for find-like methods,
# where the object is resolved from the provided params (e.g., ID)
field :home, Home, null: false, authorize: true do
argument :id, ID, required: true
end
def home(id:)
Home.find(id)
end
# Without `authorize: true` the code would look like this
def home(id:)
Home.find(id).tap { |home| authorize! home, to: :show? }
endYou can use authorization options to customize the behaviour, e.g. authorize: {to: :preview?, with: CustomPolicy}.
By default, if a user is not authorized to access the field, an ActionPolicy::Unauthorized exception is raised.
If you want to return a nil instead, you should add raise: false to the options:
# NOTE: don't forget to mark your field as nullable
field :home, Home, null: true, authorize: {raise: false}You can make non-raising behaviour a default by setting a configuration option:
ActionPolicy::GraphQL.authorize_raise_exception = falseYou can also change the default show? rule globally:
ActionPolicy::GraphQL.default_authorize_rule = :show_graphql_field?If you want to perform authorization before resolving the field value, you can use preauthorize: * option:
field :homes, [Home], null: false, preauthorize: {with: HomePolicy}
def homes
Home.all
endThe code above is equal to:
field :homes, [Home], null: false
def homes
authorize! "homes", to: :index?, with: HomePolicy
Home.all
endYou can specify the default raising behaviour for preauthorize: by setting a configuration option:
# By default, it fallbacks to .authorize_raise_exception
ActionPolicy::GraphQL.preauthorize_raise_exception = falseNOTE: we pass the field's name as the record to the policy rule. We assume that pre-authorization rules do not depend on the record itself and pass the field's name for debugging purposes only.
You can customize the authorization options, e.g. authorize: {to: :preview?, with: CustomPolicy}.
NOTE: unlike authorize: * you MUST specify the with: SomePolicy option. The default authorization rule depends on the type of the field:
- for lists we use
index?(configured byActionPolicy::GraphQL.default_preauthorize_list_ruleparameter) - for singleton fields we use
show?(configured byActionPolicy::GraphQL.default_preauthorize_node_ruleparameter)
Class-level authorization
You can use Action Policy in the class-level authorization hooks (self.authorized?) like this:
class Types::Friendship < Types::BaseObject
def self.authorized?(object, context)
super &&
allowed_to?(
:show?,
object,
# NOTE: you must provide context explicitly
context: {user: context[:current_user]}
)
end
endAuthorizing Mutations
A mutation is just a Ruby class with a single API method. There is nothing specific in authorizing mutations: from the Action Policy point of view, they are just behaviours.
If you want to authorize the mutation, you call authorize! method. For example:
class Mutations::DestroyUser < Types::BaseMutation
argument :id, ID, required: true
def resolve(id:)
user = User.find(id)
# Raise an exception if the user has not enough permissions
authorize! user, to: :destroy?
# Or check without raising and do what you want
#
# if allowed_to?(:destroy?, user)
user.destroy!
{deleted_id: user.id}
end
endCheck out this issue on how you can implement a verify_authorized callback for your mutations: #28.
Using preauthorize: * with mutations
Since mutation is also a GraphQL field, we can also use our custom authorize: * and preauthorize: * options. However, using authorize: * for mutations is deprecated and will raise an error in the future versions: it doesn't make any sense because it's called after the field has been resolved (i.e., mutation has been executed).
It is possible to override the default raising behaviour for mutation only via the following configuration option:
# By default, it fallbacks to .preauthorize_raise_exception
ActionPolicy::GraphQL.preauthorize_mutation_raise_exception = trueHandling exceptions
The query would fail with ActionPolicy::Unauthorized exception when using authorize: true (in raising mode) or calling authorize! explicitly.
That could be useful to handle this exception and send a more detailed error message to the client, for example:
Please make sure you have added error handling to your schema with use GraphQL::Execution::Errors.
# in your schema file
rescue_from(ActionPolicy::Unauthorized) do |exp|
raise GraphQL::ExecutionError.new(
# use result.message (backed by i18n) as an error message
exp.result.message,
# use GraphQL error extensions to provide more context
extensions: {
code: :unauthorized,
fullMessages: exp.result.reasons.full_messages,
details: exp.result.reasons.details
}
)
endScoping Data
You can add authorized_scope: true option to a field (list or connection) to apply the corresponding policy rules to the data:
class CityType < ::Common::Graphql::Type
# It would automatically apply the relation scope from the EventPolicy to
# the relation (city.events)
field :events, EventType.connection_type,
null: false,
authorized_scope: true
# you can specify the policy explicitly
field :events, EventType.connection_type,
null: false,
authorized_scope: {with: CustomEventPolicy}
# without the option you would write the following code
def events
authorized_scope object.events
# or if `with` option specified
authorized_scope object.events, with: CustomEventPolicy
end
endNOTE: you cannot use authorize: * and authorized_scope: * at the same time but you can combine preauthorize: * with authorized_scope: *.
See the documentation on scoping.
Exposing Authorization Rules
With action_policy-graphql gem, you can easily expose your authorization logic to the client in a standardized way.
For example, if you want to "tell" the client which actions could be performed against the object you can use the expose_authorization_rules macro to add authorization-related fields to your type:
class ProfileType < Types::BaseType
# Adds can_edit, can_destroy fields with
# AuthorizationResult type.
# NOTE: prefix "can_" is used by default, no need to specify it explicitly
expose_authorization_rules :edit?, :destroy?, prefix: "can_"
endNOTE: you can use aliases here as well as defined rules.
NOTE: This feature relies the failure reasons and the i18n integration extensions. If your policies don't include any of these, you won't be able to use it.
Then the client could perform the following query:
{
post(id: $id) {
canEdit {
# (bool) true|false; not null
value
# top-level decline message ("Not authorized" by default); null if value is true
message
# detailed information about the decline reasons; null if value is true or you don't have "failure reasons" extension enabled
reasons {
details # JSON-encoded hash of the form { "event" => [:privacy_off?] }
fullMessages # Array of human-readable reasons
}
}
canDestroy {
# ...
}
}
}You can override a custom authorization field prefix (can_):
ActionPolicy::GraphQL.default_authorization_field_prefix = "allowed_to_"You can specify a custom field name as well (only for a single rule):
class ProfileType < ::Common::Graphql::Type
# Adds can_create_post field.
expose_authorization_rules :create?, with: PostPolicy, field_name: "can_create_post"
endCustom Behaviour
Including the default ActionPolicy::GraphQL::Behaviour is equal to adding the following to your base class:
class Types::BaseObject < GraphQL::Schema::Object
# include Action Policy behaviour and its extensions
include ActionPolicy::Behaviour
include ActionPolicy::Behaviours::ThreadMemoized
include ActionPolicy::Behaviours::Memoized
include ActionPolicy::Behaviours::Namespaced
# define authorization context
authorize :user, through: :current_user
# add a method helper to get the current_user from the context
def current_user
context[:current_user]
end
# extend the field class to add `authorize` and `authorized_scope` options
field_class.prepend(ActionPolicy::GraphQL::AuthorizedField)
# add `expose_authorization_rules` macro
include ActionPolicy::GraphQL::Fields
endFeel free to create your own behaviour by adding only the functionality you need.