Skip to content

Instantly share code, notes, and snippets.

@bahodge
Last active September 26, 2019 16:32
Show Gist options
  • Save bahodge/925f95b3f70b5a50adf856838b362bed to your computer and use it in GitHub Desktop.
Save bahodge/925f95b3f70b5a50adf856838b362bed to your computer and use it in GitHub Desktop.
Metaprogramming a policy type for graphql with rails
# graphql_example.graphql
# user query
query MY_USER_QUERY($id: ID!) {
user(id: $id) {
id
policy {
isAdmin
isCreator
...
}
}
}
######################
"data": {
"id": "asdfasdfasdfasd",
"policy": {
"isAdmin": true
"isCreator": true
}
}
# models/concerns/policy_builder.rb
module PolicyBuilder
extend ActiveSupport::Concern
class PolicyError < StandardError
end
attr_reader :resource, :user_policy
module ClassMethods
def build_from_resource(resource:, user_policy: nil)
self.new(resource: resource, user_policy: user_policy)
end
end
# accepts a symbol
def check_policy(test: )
unless self.try(test)
raise PolicyError, "You do not have permission to take action!"
end
end
def initialize(resource:, user_policy: nil)
@resource = resource
@user_policy = user_policy
end
end
# graphql/types/policy_builder_type.rb
# this is the class that builds the graphql type for you
module Types
class PolicyBuilderType
def self.build(klass:)
klass_name = klass.name
Types::PolicyBuilder.build_graphql_policy(klass_name: klass_name)
end
def self.build_graphql_policy(klass_name:)
## Look inside the policy namespace
policy_klass = "::Policies::#{klass_name}Policy".constantize
## Get all of the methods that have been defined in the policy class
field_methods = policy_klass.instance_methods(false)
fixed_name = klass_name.gsub(':', '')
graphql_klass = Class.new(Types::BaseObject) do
# build policy type
graphql_name "#{fixed_name}PolicyType"
field_methods.each do |method|
# remove the '?' mark from policy methods
sanitized_method = method.to_s.gsub('?', '').to_sym
# create a new field
field sanitized_method, ::Types::BaseScalar::Boolean, null: false, method: method
end
end
end
end
end
# models/concerns/policy_manager.rb
module PolicyManager
extend ActiveSupport::Concern
module ClassMethods
end
def enforced_policy(current_user: )
user_policy = current_user.policy
policy_class.build_from_resource(resource: self, user_policy: user_policy)
end
def policy
policy_class.build_from_resource(resource: self)
end
def policy_class
klass_name = self.class.name
policy_klass = "Policies::#{klass_name}Policy".constantize
end
end
# ruby_example.rb
### This policy is based on the user itself with no other resource
user = User.first #=> #<User id: 1 ...>
user.policy #=> #<Policies::UserPolicy:0x00005597dff983d8 @resource=#<User id: 1>, user_policy=nil>
user.policy.is_creator? #=> true
### ////////////////////////////////////////////////////////////////// ###
### This policy is for a client with the context of having a current user
client = Client.last #=> #<Client id: 1 ...>
# Given a user who is allowed to create a client
admin_user = User.first #=> #<User id: 1 ...>
client_policy = client.enforced_policy(current_user: admin_user)
client_policy.can_update_client? #=> true
### Given a user who is not allowed to create a client
non_admin_user = User.last #=> #<User id: 2...>
bad_client_policy = client.enforced_policy(current_user: non_admin_user)
bad_client_policy.can_update_client? #=> false
# models/user.rb
class User < ApplicationRecord
include GraphqlManager
include PolicyManager
...
end
# models/policies/user_policy.rb
module Policies
class UserPolicy
include PolicyBuilder
def is_creator?
return false if is_view_only?
is_admin? || is_receiver? || is_technician? || is_reviewer?
end
def is_destroyer?
return false if is_view_only?
is_admin? || is_receiver? || is_technician? || is_reviewer?
end
def is_editor?
return false if is_view_only?
is_admin? || is_receiver? || is_technician? || is_reviewer?
end
def is_admin?
resource.has_role?(:admin)
end
...
end
end
# graphql/types/user_type.rb
module Types
class UserType < BaseObject
graphql_name 'UserType'
### this is graphql's node interface and isn't necessary for this ###
global_id_field :id
implements GraphQL::Relay::Node.interface
### this is graphql's node interface and isn't necessary for this ###
field :email, String, null: false
field :username, String, null: false
field :roles, [String], null: false
field :formatted_roles, String, null: false
# This is the policy builder that will build your Policy Type
# In this case it will build a graphql type called: UserPolicyType
field :policy, PolicyBuilderType.build(klass: User), null: false
...
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment