Last active
September 26, 2019 16:32
-
-
Save bahodge/925f95b3f70b5a50adf856838b362bed to your computer and use it in GitHub Desktop.
Metaprogramming a policy type for graphql with rails
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
# 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 | |
} | |
} |
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
# 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 |
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
# 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 |
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
# 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 |
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
# 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 |
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
# models/user.rb | |
class User < ApplicationRecord | |
include GraphqlManager | |
include PolicyManager | |
... | |
end |
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
# 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 |
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
# 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