diff --git a/Gemfile b/Gemfile index 96697d841..8880a7e90 100644 --- a/Gemfile +++ b/Gemfile @@ -10,18 +10,17 @@ gem 'rails', '~> 6.1' # Use postgresql as the database for Active Record gem 'pg', '>= 0.18', '< 2.0' -gem 'graphql', '~> 1.12.17' +gem 'graphql', '~> 2.0.0' gem 'graphql-batch' -gem 'graphql-guard' # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker gem 'webpacker', '~> 6.0.0.rc.5' gem 'react-rails' -gem 'devise', '~> 4.8.0' -gem 'omniauth-bottlenose', git: 'https://github.com/CodeGrade/omniauth-bottlenose' -gem 'omniauth-oauth2', '~> 1.7.0' +gem 'devise', '~> 4.9.0' +gem 'omniauth-bottlenose', git: 'https://github.com/CodeGrade/omniauth-bottlenose', ref: '6202663' +gem 'omniauth-oauth2', '~> 1.8.0' gem 'omniauth-rails_csrf_protection' gem 'bootstrap_form', '>= 4.2.0' @@ -44,7 +43,7 @@ gem 'pretender' gem 'bootsnap', '>= 1.4.2', require: false gem 'activerecord_json_validator', '~> 2.0' -gem 'json-schema', '~> 2.8' +gem 'json-schema', '~> 4.0' gem 'listen', '~> 3.3' diff --git a/Gemfile.lock b/Gemfile.lock index c20d5bb69..dd5341e1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,10 @@ GIT remote: https://github.com/CodeGrade/omniauth-bottlenose - revision: 3e7c696c9b1da6823f5936a5bc7b061f3630720a + revision: 62026630bf96874214a3b20b2c859bad378a6d77 + ref: 6202663 specs: - omniauth-bottlenose (0.1.0) - omniauth-oauth2 (~> 1.7.0) + omniauth-bottlenose (0.2.0) + omniauth-oauth2 (~> 1.8.0) GEM remote: https://rubygems.org/ @@ -114,7 +115,7 @@ GEM database_cleaner-core (2.0.1) date (3.3.3) debug_inspector (1.1.0) - devise (4.8.1) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -136,12 +137,10 @@ GEM ffi (1.15.5) globalid (1.1.0) activesupport (>= 5.0) - graphql (1.12.24) + graphql (2.0.21) graphql-batch (0.5.2) graphql (>= 1.10, < 3) promise.rb (~> 0.7.2) - graphql-guard (2.0.0) - graphql (>= 1.10.0, < 2) hana (1.3.7) hashie (5.0.0) headless (2.3.1) @@ -149,8 +148,8 @@ GEM concurrent-ruby (~> 1.0) interception (0.5) json (2.6.3) - json-schema (2.8.1) - addressable (>= 2.4) + json-schema (4.0.0) + addressable (>= 2.8) json_schemer (0.2.24) ecma-re-validator (~> 0.3) hana (~> 1.3) @@ -203,9 +202,9 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-oauth2 (1.7.3) + omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) - omniauth (>= 1.9, < 3) + omniauth (~> 2.0) omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) @@ -363,17 +362,16 @@ DEPENDENCIES byebug capybara database_cleaner - devise (~> 4.8.0) + devise (~> 4.9.0) factory_bot_rails - graphql (~> 1.12.17) + graphql (~> 2.0.0) graphql-batch - graphql-guard headless - json-schema (~> 2.8) + json-schema (~> 4.0) listen (~> 3.3) minitest-reporters omniauth-bottlenose! - omniauth-oauth2 (~> 1.7.0) + omniauth-oauth2 (~> 1.8.0) omniauth-rails_csrf_protection passenger (>= 5.3.2) pg (>= 0.18, < 2.0) diff --git a/app/graphql/hourglass_schema.rb b/app/graphql/hourglass_schema.rb index 0f26d4c08..7f611fc6f 100644 --- a/app/graphql/hourglass_schema.rb +++ b/app/graphql/hourglass_schema.rb @@ -15,11 +15,11 @@ class HourglassSchema < GraphQL::Schema # Feedback and error messages in development mode will be # more informative than in production/test modes. if Rails.env.development? - use GraphQL::Guard.new( - not_authorized: lambda do |type, field| - GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") - end, - ) + # use GraphQL::Guard.new( + # not_authorized: lambda do |type, field| + # GraphQL::ExecutionError.new("Not authorized to access #{type}.#{field}") + # end, + # ) def self.unauthorized_object(error) # Add a top-level error to the response instead of returning nil: @@ -38,11 +38,11 @@ def self.execute(query_str = nil, **kwargs) super(query_str, **kwargs) end else - use GraphQL::Guard.new( - not_authorized: lambda do |type, field| - GraphQL::ExecutionError.new("You do not have permission to view that data.") - end, - ) + # use GraphQL::Guard.new( + # not_authorized: lambda do |type, field| + # GraphQL::ExecutionError.new("You do not have permission to view that data.") + # end, + # ) def self.unauthorized_object(error) # Add a top-level error to the response instead of returning nil: diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 611eb0560..ba3667f01 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -3,5 +3,18 @@ module Types class BaseField < GraphQL::Schema::Field argument_class Types::BaseArgument + def guard(proc) + @guard_proc = proc + end + + def authorized?(obj, args, ctx) + return true unless @guard_proc + + wrapped = Types::GuardWrapper.new(self, obj) + answer = @guard_proc.call(wrapped, args, ctx) + return true if answer + + raise GraphQL::ExecutionError, "Not authorized to access #{self.owner_type.graphql_name}.#{self.graphql_name}." + end end end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index a69458deb..426f962d9 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -1,10 +1,33 @@ # frozen_string_literal: true module Types + class GuardWrapper + attr_accessor :class, :object + + def initialize(cls, object) + @class = cls + @object = object + end + end + # The base class of Hourglass objects returned by GraphQL class BaseObject < GraphQL::Schema::Object field_class Types::BaseField + def self.guard(proc) + @guard_proc = proc + end + + def self.authorized?(obj, ctx) + return true unless @guard_proc + + wrapped = Types::GuardWrapper.new(self, obj) + answer = @guard_proc.call(wrapped, nil, ctx) + return true if answer + + raise GraphQL::ExecutionError, "You do not have permission to view that information." + end + module Guards def self.exam_role(user, ctx) ctx[:access_cache]&.dig(:role_for_exam, user.id) || Exam.roles[:no_reg] diff --git a/app/graphql/types/exam_version_type.rb b/app/graphql/types/exam_version_type.rb index 6847c67c9..44a599baf 100644 --- a/app/graphql/types/exam_version_type.rb +++ b/app/graphql/types/exam_version_type.rb @@ -17,7 +17,7 @@ class ExamVersionType < Types::BaseObject [ q, q.parts.map do |p| [p, p.body_items.to_a] end ] end.flatten all_qpbs.each do |qpb| - resolved = HourglassSchema.resolve_type(qpb.class, qpb, ctx) + resolved, _ = HourglassSchema.resolve_type(qpb.class, qpb, ctx) Guards.cache( ctx[:access_cache], [resolved.name, qpb.id, :visible, ctx[:current_user].id], @@ -25,7 +25,7 @@ class ExamVersionType < Types::BaseObject ) end [obj.object.rubrics, obj.object.db_references, obj.object.rubric_presets, obj.object.preset_comments].flatten.each do |r| - resolved = HourglassSchema.resolve_type(r.class, r, ctx) + resolved, _ = HourglassSchema.resolve_type(r.class, r, ctx) Guards.cache( ctx[:access_cache], [resolved.name, r.id, :visible, ctx[:current_user].id], diff --git a/app/graphql/types/grading_comment_type.rb b/app/graphql/types/grading_comment_type.rb index fae614cae..5d90c23f5 100644 --- a/app/graphql/types/grading_comment_type.rb +++ b/app/graphql/types/grading_comment_type.rb @@ -26,6 +26,5 @@ def bnum field :message, String, null: false field :points, Float, null: false field :creator, Types::UserType, null: false - field :preset_comment, Types::PresetCommentType, null: true end end diff --git a/app/graphql/types/grading_lock_type.rb b/app/graphql/types/grading_lock_type.rb index 7435f66e1..c53c3904c 100644 --- a/app/graphql/types/grading_lock_type.rb +++ b/app/graphql/types/grading_lock_type.rb @@ -3,24 +3,31 @@ module Types class GradingLockType < Types::BaseObject implements GraphQL::Types::Relay::Node - global_id_field :id - field :id, ID, null: false, guard: ->(_obj, _args, _ctx) { true } + global_id_field :id - guard Guards::VISIBILITY + guard Guards::ALL_STAFF - field :registration, Types::RegistrationType, null: false + field :registration, Types::RegistrationType, null: false do + guard Guards::VISIBILITY + end def registration RecordLoader.for(Registration).load(object.registration_id) end - field :grader, Types::UserType, null: true, guard: ->(obj, _args, ctx) { - obj.object.grader_id.nil? || obj.object.visible_to?(ctx[:current_user], Guards.exam_role(ctx[:current_user], ctx), Guards.course_role(ctx[:current_user], ctx)) - } + + field :grader, Types::UserType, null: true do + guard ->(obj, _args, ctx) { + obj.object.grader_id.nil? || obj.object.visible_to?(ctx[:current_user], Guards.exam_role(ctx[:current_user], ctx), Guards.course_role(ctx[:current_user], ctx)) + } + end def grader RecordLoader.for(User).load(object.grader_id) end - field :completed_by, Types::UserType, null: true, guard: ->(obj, _args, ctx) { - obj.object.completed_by_id.nil? || obj.object.visible_to?(ctx[:current_user], Guards.exam_role(ctx[:current_user], ctx), Guards.course_role(ctx[:current_user], ctx)) - } + + field :completed_by, Types::UserType, null: true do + guard ->(obj, _args, ctx) { + obj.object.completed_by_id.nil? || obj.object.visible_to?(ctx[:current_user], Guards.exam_role(ctx[:current_user], ctx), Guards.course_role(ctx[:current_user], ctx)) + } + end def completed_by RecordLoader.for(User).load(object.completed_by_id) end @@ -29,6 +36,7 @@ def completed_by def qnum RecordLoader.for(Question).load(object.question_id).then{|q| q.index} end + field :pnum, Integer, null: false def pnum RecordLoader.for(Part).load(object.part_id).then{|q| q.index} diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 721cc3a20..d5db0f13b 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -4,8 +4,8 @@ module Types class QueryType < Types::BaseObject # Add root-level fields here. # They will be entry points for queries on your schema. - add_field(GraphQL::Types::Relay::NodeField) - add_field(GraphQL::Types::Relay::NodesField) + include GraphQL::Types::Relay::HasNodeField + include GraphQL::Types::Relay::HasNodesField field :impersonating, Boolean, null: false def impersonating diff --git a/app/models/grading_lock.rb b/app/models/grading_lock.rb index 6dd9261eb..494fb2faf 100644 --- a/app/models/grading_lock.rb +++ b/app/models/grading_lock.rb @@ -25,6 +25,7 @@ def valid_qp delegate :exam_version, to: :registration delegate :exam, to: :registration + delegate :course, to: :exam scope :incomplete, -> { where(completed_by: nil) } scope :complete, -> { where.not(completed_by: nil) } diff --git a/lib/tasks/sample_data.rake b/lib/tasks/sample_data.rake index 34f914eff..2b8eb7bbb 100644 --- a/lib/tasks/sample_data.rake +++ b/lib/tasks/sample_data.rake @@ -15,6 +15,7 @@ end NUM_SIM_USERS = 1000 def create_simulation_users(lecture:, lab:, room:, exam_version:) + puts "Creating simulation users..." (0..NUM_SIM_USERS).each do |i| student = create(:user, username: "stresstest#{i}") create(:student_registration, user: student, section: lecture) @@ -26,6 +27,7 @@ end def make_sample_data ActiveRecord::Base.transaction do + puts "Loading sample data" create(:admin, username: 'admin') make_cs2500 make_cs3500 @@ -33,6 +35,7 @@ def make_sample_data end def make_cs2500 + puts "Creating CS2500..." fall2021 = create(:term, year: 2021, semester: Term.semesters['fall']) cs2500 = create(:course, title: 'CS 2500', term: fall2021) cs2500lec = create(:section, :lecture, course: cs2500) @@ -82,6 +85,7 @@ def make_cs2500 end def make_cs3500 + puts "Creating CS3500..." spring2022 = create(:term, year: 2022, semester: Term.semesters['spring']) cs3500 = create(:course, title: 'CS 3500', term: spring2022) cs3500lec = create(:section, :lecture, course: cs3500)