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.
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
end
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
.
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? }
end
You 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 = false
You 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
end
The code above is equal to:
field :homes, [Home], null: false
def homes
authorize! "homes", to: :index?, with: HomePolicy
Home.all
end
You 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 = false
NOTE: 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_rule
parameter) - for singleton fields we use
show?
(configured byActionPolicy::GraphQL.default_preauthorize_node_rule
parameter)
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
end
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
end
Check out this issue on how you can implement a verify_authorized
callback for your mutations: #28.
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 = true
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
}
)
end
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
end
NOTE: you cannot use authorize: *
and authorized_scope: *
at the same time but you can combine preauthorize: *
with authorized_scope: *
.
See the documentation on scoping.
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_"
end
NOTE: 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"
end
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
end
Feel free to create your own behaviour by adding only the functionality you need.