Skip to content

Instantly share code, notes, and snippets.

@scottserok
Last active May 8, 2019 17:52
Show Gist options
  • Save scottserok/4a77ec3d31d10b125d76a61d1dab4b96 to your computer and use it in GitHub Desktop.
Save scottserok/4a77ec3d31d10b125d76a61d1dab4b96 to your computer and use it in GitHub Desktop.
Tiny GraphQL server to share authentication and authorization pattern for mutations
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'graphql'
gem 'rack'
gem 'webrick'
gem 'mail'
end
require 'json'
SPLASH = <<-TXT
Use a local SMTP program like `mailcatcher` to receive and display email messages.
$ gem install mailcatcher && mailcatcher -f
Test the application using the `curl` program.
curl -s -X POST localhost:3000/graphql \\
-d 'query=mutation { subscribe(email:"scott@serok.us") { emailList } }'
curl -s -X POST localhost:3000/graphql \\
-d 'query=mutation { subscribe(email:"scott@serok.us") { emailList } }' \\
-H 'x-email: scott@serok.us'
curl -s -X POST localhost:3000/graphql \\
-d 'query=mutation { subscribe(email:"jacob@serok.us") { emailList } }' \\
-H 'x-email: scott@serok.us'
curl -s -X POST localhost:3000/graphql \\
-d 'query=mutation { sendEmail(text:"Welcome everyone") { emailList } }' \\
-H 'x-email: scott@serok.us'
curl -s -X POST localhost:3000/graphql \\
-d 'query=mutation { sendEmail(text:"Welcome everyone") { emailList } }' \\
-H 'x-email: scott@serok.us' -H 'x-admin: true'
TXT
class UnauthorizedError < StandardError
def initialize
super "unauthorized"
end
end
class Newsletter
def self.list
$email_list.to_a
end
def self.subscribe(email)
$email_list << email
end
def self.send_mail(message)
Mail.deliver do
from EMAIL_FROM
to EMAIL_FROM
bcc $email_list.to_a
subject 'New email from list'
body message
end
end
EMAIL_FROM = 'no-reply@serok.us'.freeze
end
module Mutations
# Implement #resolve for all subclasses with auth methods.
# Base mutation only cares that a user is authenticated.
# Child classes should implement #call instead.
class BaseMutation < ::GraphQL::Schema::Mutation
def resolve(args)
authenticate_user!
puts 'authenticated!'
authorize_user!
puts 'authorized!'
call args
rescue => e
puts self.class.to_s + ' ' + e.message.to_s
GraphQL::ExecutionError.new e.message.to_json
end
def authenticate_user!
context.current_user || raise(UnauthorizedError.new)
end
def authorize_user!; end
end
# Extension of the base class that implements the authorization method to
# ensure only certain types of users can perform the mutation.
class AdminMutation < BaseMutation
# override per mutation if you need more granularity before executing the #call method
def authorize_user!
context.admin? || raise(UnauthorizedError.new)
end
end
# Send an email to all email addresses subscribed to the newsletter
class SendEmail < AdminMutation
argument :text, String, required: true
field :email_list, [String], null: false
def call(args)
params = args.slice :text
Newsletter.send_mail params[:text]
{ email_list: Newsletter.list }
end
end
# Subscribe the given email address if the user is authenticated
class Subscribe < BaseMutation
argument :email, String, required: true
field :email_list, [String], null: false
def call(args)
params = args.slice :email, :role
Newsletter.subscribe params[:email]
{ email_list: Newsletter.list }
end
end
end
# Mutate the state of our Newsletter
class MutationType < GraphQL::Schema::Object
field :send_email, mutation: Mutations::SendEmail
field :subscribe, mutation: Mutations::Subscribe
end
# Query information about the Newsletter
class QueryType < GraphQL::Schema::Object
field :email_list, [String], null: false
def email_list
Newsletter.list
end
end
# Simple request context used for authentication and authorization
class Context < GraphQL::Query::Context
def current_user
@current_user ||= self[:email]
end
def admin?
@admin ||= self[:admin]
end
end
# Our GraphQL Schema
class Schema < GraphQL::Schema
context_class ::Context
mutation ::MutationType
query ::QueryType
end
# Rack compliant application serving the GraphQL Schema class
class Application
def self.call(env)
query = env['rack.input'].gets&.split("query=")[1]
context = {}
context[:email] = env['HTTP_X_EMAIL'] # sad authentication mechanism
context[:admin] = true if env['HTTP_X_ADMIN'] # sad authorization via HTTP header flag
result = Schema.execute query, context: context
[200, { 'Content-Type' => 'application/json' }, [result.to_json]]
end
end
# Initialize the mail defaults
Mail.defaults do
delivery_method :smtp, address: 'localhost', port: 1025
end
# Initialize the temporary database
$email_list = Set.new
puts SPLASH
# Initialize the web server mounting Application at /graphql
options = { Port: 3000 }
server = ::WEBrick::HTTPServer.new(options)
server.mount '/graphql', Rack::Handler::WEBrick, Application
server.start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment