Skip to content

Commit

Permalink
fixes #23678 - add graphql scaffolding
Browse files Browse the repository at this point in the history
  • Loading branch information
timogoebel authored and xprazak2 committed Feb 26, 2019
1 parent 4d85cea commit d1e1858
Show file tree
Hide file tree
Showing 24 changed files with 503 additions and 26 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ gem 'daemons'
gem 'get_process_mem'
gem 'rack-cors', '~> 1.0.2', require: 'rack/cors'
gem 'jwt', '~> 2.1.0'
gem 'graphql', '~> 1.8.0'
gem 'graphql-batch'

Dir["#{File.dirname(FOREMAN_GEMFILE)}/bundler.d/*.rb"].each do |bundle|
self.instance_eval(Bundler.read_file(bundle))
Expand Down
87 changes: 87 additions & 0 deletions app/controllers/api/graphql_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module Api
class GraphqlController < ActionController::Base
include Foreman::ThreadSession::Cleaner
include Foreman::Controller::Timezone
include Foreman::Controller::RequireSsl
include Foreman::Controller::Session
include Foreman::Controller::Authentication

rescue_from Exception, :with => :generic_exception if Rails.env.production?

before_action :authenticate
around_action :set_timezone

def execute
result = if params[:_json]
execute_multiplexed_graphql_query
else
execute_single_graphql_query
end

render json: result
end

def api_request?
true
end

private

def execute_multiplexed_graphql_query
current_user = User.current
queries = params[:_json].map do |param|
{
query: param['query'],
operation_name: param['operationName'],
variables: ensure_hash(param['variables']),
context: {
current_user: current_user,
request_id: request.uuid
}
}
end
ForemanGraphqlSchema.multiplex(queries)
end

def execute_single_graphql_query
ForemanGraphqlSchema.execute(
params[:query],
variables: variables,
context: {
current_user: User.current,
request_id: request.uuid
}
)
end

def variables
ensure_hash(params[:variables])
end

def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when ActionController::Parameters
ambiguous_param.to_unsafe_h
when Hash
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
rescue JSON::ParserError
raise ArgumentError, "Could not parse JSON data in #{ambiguous_param}"
end

def generic_exception(exception)
Foreman::Logging.exception('Action failed', exception)
render json: "{ 'error': 500 }", :status => :internal_server_error
end
end
end
6 changes: 1 addition & 5 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ class ApplicationController < ActionController::Base

include Foreman::Controller::Flash
include Foreman::Controller::Authorize
include Foreman::Controller::RequireSsl

force_ssl :if => :require_ssl?
protect_from_forgery # See ActionController::RequestForgeryProtection for details
rescue_from Exception, :with => :generic_exception if Rails.env.production?
rescue_from ScopedSearch::QueryNotSupported, :with => :invalid_search_query
Expand Down Expand Up @@ -75,10 +75,6 @@ def deny_access
(User.current.logged? || request.xhr?) ? render_403 : require_login
end

def require_ssl?
SETTINGS[:require_ssl]
end

# This filter is called before FastGettext set_gettext_locale and sets user-defined locale
# from db. It must be called after require_login.
def set_gettext_locale_db
Expand Down
17 changes: 7 additions & 10 deletions app/controllers/concerns/application_shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@ module ApplicationShared
include Foreman::Controller::Authentication
include Foreman::Controller::Session
include Foreman::Controller::TopbarSweeper
include Foreman::Controller::Timezone
include Foreman::ThreadSession::Cleaner
include FindCommon

def set_timezone
default_timezone = Time.zone
client_timezone = User.current.try(:timezone) || cookies[:timezone]
Time.zone = client_timezone if client_timezone.present?
yield
ensure
# Reset timezone for the next thread
Time.zone = default_timezone
end

def current_permission
[action_permission, controller_permission].join('_')
end

def set_current_user(user)
super
set_taxonomy
user.present?
end

def set_taxonomy
TopbarSweeper.expire_cache
user = User.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ def set_current_user(user)
session[:user] = user.id
update_activity_time
end
set_taxonomy
user.present?
end
end
17 changes: 17 additions & 0 deletions app/controllers/concerns/foreman/controller/require_ssl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Foreman
module Controller
module RequireSsl
extend ActiveSupport::Concern

included do
force_ssl :if => :require_ssl?
end

protected

def require_ssl?
SETTINGS[:require_ssl]
end
end
end
end
17 changes: 17 additions & 0 deletions app/controllers/concerns/foreman/controller/timezone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Foreman
module Controller
module Timezone
extend ActiveSupport::Concern

def set_timezone
default_timezone = Time.zone
client_timezone = User.current.try(:timezone) || cookies[:timezone]
Time.zone = client_timezone if client_timezone.present?
yield
ensure
# Reset timezone for the next thread
Time.zone = default_timezone
end
end
end
end
7 changes: 7 additions & 0 deletions app/graphql/foreman_graphql_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ForemanGraphqlSchema < GraphQL::Schema
# Set up the graphql-batch gem
lazy_resolve(Promise, :sync)
use GraphQL::Batch

query(Types::Query)
end
44 changes: 44 additions & 0 deletions app/graphql/queries/authorized_model_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Queries
class AuthorizedModelQuery
def initialize(model_class:, user:)
@model_class = model_class
@user = user
end

delegate :find_by, to: :authorized_scope

def results(params = {})
if params[:search].present?
authorized_scope.search_for(*search_options(params))
elsif params[:orderField].present?
ordered_results(order_field: params[:orderField], order_direction: params[:orderDirection])
else
authorized_scope.all
end
end

private

attr_reader :model_class, :user

def authorized_scope
return model_class unless model_class.respond_to?(:authorized)

permission = model_class.find_permission_name(:view)
model_class.authorized_as(user, permission, model_class)
end

def search_options(params)
search_options = [params[:search]]
if params[:orderField].present?
search_options << { :order => "#{params[:orderField]} #{params[:orderDirection]}".strip }
end
search_options
end

def ordered_results(order_field:, order_direction:)
order_direction = order_direction.presence || :ASC
authorized_scope.order(order_field => order_direction).all
end
end
end
17 changes: 17 additions & 0 deletions app/graphql/queries/fetch_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Queries
class FetchField < GraphQL::Function
attr_reader :type

argument(:id, !types.Int, 'ID for Record')

def initialize(model_class:, type:)
@model_class = model_class
@type = type
end

def call(_, args, ctx)
Queries::AuthorizedModelQuery.new(model_class: @model_class, user: ctx[:current_user])
.find_by(id: args['id'])
end
end
end
20 changes: 20 additions & 0 deletions app/graphql/queries/plural_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Queries
class PluralField < GraphQL::Function
attr_reader :type

argument(:search, types.String, 'Search query')
argument(:orderField, types.String, 'Order field')
argument(:orderDirection, types.String, 'Order direction')

def initialize(model_class:, type:)
@model_class = model_class
@type = type
end

def call(_, args, ctx)
params = args.to_h.slice('search', 'orderField', 'orderDirection').symbolize_keys
Queries::AuthorizedModelQuery.new(model_class: @model_class, user: ctx[:current_user])
.results(params)
end
end
end
4 changes: 4 additions & 0 deletions app/graphql/types/base_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Types
class BaseObject < GraphQL::Types::Relay::BaseObject
end
end
11 changes: 11 additions & 0 deletions app/graphql/types/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Types
class Model < BaseObject
description 'A Model'

field :id, Integer, null: false
field :name, String, null: false
field :info, String, null: true
field :vendorClass, String, null: true
field :hardwareModel, String, null: true
end
end
11 changes: 11 additions & 0 deletions app/graphql/types/query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Types
class Query < GraphQL::Schema::Object
graphql_name 'Query'

field :model, Types::Model,
function: Queries::FetchField.new(type: Types::Model, model_class: ::Model), null: true

field :models, Types::Model.connection_type, connection: true,
function: Queries::PluralField.new(type: Types::Model, model_class: ::Model)
end
end
22 changes: 13 additions & 9 deletions app/models/concerns/authorizable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,7 @@ def authorized?(permission)
end

def permission_name(action)
type = Permission.resource_name(self.class)
permissions = Permission.where(:resource_type => type).where(["#{Permission.table_name}.name LIKE ?", "#{action}_%"])

# some permissions are grouped for same resource, e.g. edit_comupute_resources and edit_compute_resources_vms, in such case we need to detect the right permission
if permissions.size > 1
permissions.detect { |p| p.name.end_with?(type.underscore.pluralize) }.try(:name)
else
permissions.first.try(:name)
end
self.class.find_permission_name(action)
end

included do
Expand Down Expand Up @@ -115,5 +107,17 @@ def skip_permission_check
ensure
Thread.current[:ignore_permission_check] = original_value
end

def find_permission_name(action)
type = Permission.resource_name(self)
permissions = Permission.where(:resource_type => type).where(["#{Permission.table_name}.name LIKE ?", "#{action}_%"])

# some permissions are grouped for same resource, e.g. edit_comupute_resources and edit_compute_resources_vms, in such case we need to detect the right permission
if permissions.size > 1
permissions.detect { |p| p.name.end_with?(type.underscore.pluralize) }.try(:name)
else
permissions.first.try(:name)
end
end
end
end
6 changes: 6 additions & 0 deletions app/models/host/managed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ def self.model_name
ActiveModel::Name.new(Host)
end

# Permissions introduced by plugins for this class can cause resource <-> permission
# names mapping to fail randomly so as a safety precaution, we specify the name more explicitly.
def self.find_permission_name(action)
"#{action}_hosts"
end

def clear_reports
# Remove any reports that may be held against this host
Report.where("host_id = #{id}").delete_all
Expand Down
2 changes: 2 additions & 0 deletions config/initializers/graphql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'graphql'
require 'graphql/batch'
5 changes: 5 additions & 0 deletions config/routes/api/graphql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Foreman::Application.routes.draw do
namespace :api, defaults: {format: 'json'} do
post '/graphql', to: 'graphql#execute'
end
end
11 changes: 11 additions & 0 deletions test/controllers/api/graphql_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'test_helper'

class Api::GraphqlControllerTest < ActionController::TestCase
test 'empty query' do
post :execute, params: {}

assert_response :success
refute_empty json_errors
assert_includes json_error_messages, 'No query string was present'
end
end
Loading

0 comments on commit d1e1858

Please sign in to comment.