Last active
May 8, 2019 17:52
-
-
Save scottserok/4a77ec3d31d10b125d76a61d1dab4b96 to your computer and use it in GitHub Desktop.
Tiny GraphQL server to share authentication and authorization pattern for mutations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:"[email protected]") { emailList } }' | |
curl -s -X POST localhost:3000/graphql \\ | |
-d 'query=mutation { subscribe(email:"[email protected]") { emailList } }' \\ | |
-H 'x-email: [email protected]' | |
curl -s -X POST localhost:3000/graphql \\ | |
-d 'query=mutation { subscribe(email:"[email protected]") { emailList } }' \\ | |
-H 'x-email: [email protected]' | |
curl -s -X POST localhost:3000/graphql \\ | |
-d 'query=mutation { sendEmail(text:"Welcome everyone") { emailList } }' \\ | |
-H 'x-email: [email protected]' | |
curl -s -X POST localhost:3000/graphql \\ | |
-d 'query=mutation { sendEmail(text:"Welcome everyone") { emailList } }' \\ | |
-H 'x-email: [email protected]' -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 = '[email protected]'.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